0x00 NDK 简介
什么是Android NDK
NDK是(Native Development Kit)的缩写,也就是开发Native的套件或者说工具集合。
NDK提供了一系列的工具,帮助开发者快速开发C/C++的动态库(.SO文件),并且能够自动将 so和java 应用一起打包成apk.
NDK存在的意义
Android 的 SDK 是基于JAVA实现,意味着基于 SDK 进行开发的应用都必须使用JAVA语言。然而 C/C++ 与 JAVA 各有各的优点何用途,为了能支持 C/C++ 谷歌在开发初期就使其 Dalvik虚拟机支持 JNI 编程方式,也就是第三方应用的 JAVA代码完全可以通过本机的(JNI)框架来调用Native动态库里的函数(.so文件里的各种函数).
也就是说 因为有 NDK 和 JNI 的支持, Android平台可以实现 “JAVA + C”的这么一种编程方式。
为什么要用 NDK (什么时候需要用 C)
- 可以方便的使用现有的开源库。(大部分的开源库都是用 C/C++ 编写的)
- 提高程序的执行效率。(很多要求高性能的应用使用C开发,从而提高应用程序的执行效率)。
- 代码的保护。(apk 的 JAVA 层代码很容易被反编译,而 C/C++ 库的反汇编难度比较大)。
- 底层程序设计。(例如,应用程序不依赖 Dalvik JAVA 虚拟机 )
0x01 JNI 简介
什么是 JNI
JNI 是一种在JAVA虚拟机控制下执行代码的标准机制。代码被编写成汇编程序或者 C/C++ 程序,并组装为动态组。也就允许了非静态绑定用法。提供了在 JAVA 平台上调用C/C++的一种途径,反之亦然。
JNI 的优势
与其它类似接口(NETSCAP JAVA 运行接口,Microsoft 的原始本地接口,COM/JAVA 接口)相比,JNI主要的竞争优势在于:
它在设计之初就确保了二进制的兼容性,JNI 编写的应用程序兼容性以及在某些平台上的 JAVA 虚拟机兼容性(不只 Dalvik 虚拟机 还有一般的 JAVA 虚拟机)。
这就是为什么 C/C++ 编译后的代码无论在任何平台上都能执行。不过一些早期版本并不支持二进制兼容。
JNI 组织结构

这张 JNI 函数表的组成就像 C++ 的虚函数表。虚拟机可以运行多张函数表,举例来说,一张调试函数表,另一张是调用函数表。JNI 接口指针仅在当前线程中起作用。
这意味着 指针不能从一个线程进入另一个线程。 然而, 可以在不同的线程中调用本地方法。
示例代码:
|
|
- *env - 一个接口指针
- obj - 在本地方法中声明的对象引用
- i 和 s - 用于传递的参数
原始类型(Primitive Type)在虚拟机和本机代码进行拷贝,对象之间使用引用进行传递。VM(虚拟机)要追踪所有传递给本地代码的对象引用。GC无法释放所有传递给本地代码的对象引用。与此同时,本机代码应该通知VM不需要的对象引用。
局部引用和全局引用
JNI 定义了三种引用类型:局部引用,全局引用和全局弱引用。
局部引用
局部引用在方法完成之前是有效的。所有通过 JNI 函数返回的 JAVA 对象都是本地引用。程序员希望 VM 会清空所有的局部引用,然而局部引用尽在其创建的线程里可用。如果有必要,局部引用可以通过接口中的 DeletteLocalRef JNI 方法立即释放:
全局引用
全局引用在完全释放之前都是有效的。要创建一个全局引用,需要调用 NewGlobalRef 方法。如果全局引用不是必须的,可以通过 DeleteGlobalRef 方法删除:
错误
JNI 不会检查 NullPointerException、IllegalArgumentException这样的错误,原因是:
- 性能下降
- 在绝大多数 C 的库函数中,很难避免错误发生。
JNI 允许用户使用 JAVA 异常处理。 大部分 JNI 方法会返回错误代码 但是本身并不会报出异常。因此,很有必要再代码本身进行处理,将异常抛给 JAVA。
在 JNI 内部,首先会检查调用函数返回的错误代码,之后会调用 ExpertOccurred() 返回一个错误对象。
JNI 原始类型
JNI 有自己的原始数据类型和数据引用类型。
| JAVA类型 | 本地类型(JNI) | 描述 |
|---|---|---|
| boolean(布尔型) | jboolean | 无符号 8 bit |
| byte(字节型) | jbyte | 有符号 8 bit |
| char(字符型) | jchar | 无符号 16 bit |
| short(短整型) | jshort | 有符号 16 bit |
| int(整形) | jint | 有符号 32 bit |
| long(长整形) | jlong | 有符号 64 bit |
| float(浮点型) | jfloat | 32 bit |
| double(双精度浮点型) | jdouble | 64 bit |
| void(空型) | void | N/A |
JNI 引用类型

改进的 UTF-8 编码
JNI 使用改进的 UTF-8 来表现不同的字符串类型。JAVA 使用 UTF-16 编码。 UTF-8 编码主要适用于 C 语言, 因为他们的编码把 u000 表示为 0xc0,而不是通常的 0x00。 使用改进的字符串可以使得 只包含非空 ASCII 的字符串编码只可以用一个字节(byte)表示。
JNI 函数
JNI 接口不仅有自己的数据集(dataset)也有自己的函数。回顾这些数据集和函数需要花费我们很多时间。可以从官方文档中找到更多信息:
JNI 函数使用示例
下面通过简短的例子确保你对这些资料所讲的内容有了正确的理解:
大体来看这是一个 C 的代码 然后他应该是从 C 这边调用 JAVA ,我们逐个分析:
JavaVM- 提供了一个接口,可以调用函数创建,删除 JAVA 虚拟机.JNIEnv- 确保了大多数的 JNI 函数。JavaVMInitArgs- Java 虚拟机参数JavaVMOption- Java 虚拟机选项
JNI 的 _CreateJavaVM() 方法初始化 JAVA 虚拟机并向 JNI 接口返回一个指针, JNI_DestroyJavaVM() 方法可以载入创建更好的 JAVA 虚拟机。
线程
内核负责管理所有在 Lniux 上运行的线程。线程通过 AttachCurrentThread 和 AttachCurrentThreadAsDaemon 函数附加到 JAVA 虚拟机。 如果线程没有被添加成功,则不能访问 JNIEnv。Android 系统不能停止 JNI 创建的线程, 即使 GC(Garbage Collection)在运行释放内存时也不行。直到调用 DetachCurrentThread 方法,该线程才会从 JAVA 虚拟机脱离。
作者 @sweatbuffer
2017 年 01月 07日