SweatBuffer`s Blog

关于Android NDK开发(一) - NDK 和 JNI 简介

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.PNG-100.8kB


JNI 组织结构

JNI-interface.jpg-50kB
这张 JNI 函数表的组成就像 C++ 的虚函数表。虚拟机可以运行多张函数表,举例来说,一张调试函数表,另一张是调用函数表。JNI 接口指针仅在当前线程中起作用。

这意味着 指针不能从一个线程进入另一个线程。 然而, 可以在不同的线程中调用本地方法。

示例代码:

1
2
3
4
5
6
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s)
{
const char *str = (*env)->GetStringUTFChars(env, s, 0);
(*env)->ReleaseStringUTFChars(env, s, str);
return 10;
}
  • *env - 一个接口指针
  • obj - 在本地方法中声明的对象引用
  • i 和 s - 用于传递的参数

原始类型(Primitive Type)在虚拟机和本机代码进行拷贝,对象之间使用引用进行传递。VM(虚拟机)要追踪所有传递给本地代码的对象引用。GC无法释放所有传递给本地代码的对象引用。与此同时,本机代码应该通知VM不需要的对象引用。


局部引用和全局引用

JNI 定义了三种引用类型:局部引用全局引用全局弱引用

局部引用

局部引用在方法完成之前是有效的。所有通过 JNI 函数返回的 JAVA 对象都是本地引用。程序员希望 VM 会清空所有的局部引用,然而局部引用尽在其创建的线程里可用。如果有必要,局部引用可以通过接口中的 DeletteLocalRef JNI 方法立即释放:

1
2
3
4
jclass clazz;
clazz = (*env)->FindClass(env, "java/lang/String");
...
(*env)->DeleteLocalRef(env, clazz)

全局引用

全局引用在完全释放之前都是有效的。要创建一个全局引用,需要调用 NewGlobalRef 方法。如果全局引用不是必须的,可以通过 DeleteGlobalRef 方法删除:

1
2
3
4
5
6
7
jclass localClazz;
jclass globalClazz;
...
localClazz = (*env)->FindClass(env, "java/lang/String");
globalClazz = (*env)->NewGlobalRef(env, localClazz);
...
(*env)->DeleteLocalRef(env, localClazz);


错误

JNI 不会检查 NullPointerExceptionIllegalArgumentException这样的错误,原因是:

  • 性能下降
  • 在绝大多数 C 的库函数中,很难避免错误发生。

JNI 允许用户使用 JAVA 异常处理。 大部分 JNI 方法会返回错误代码 但是本身并不会报出异常。因此,很有必要再代码本身进行处理,将异常抛给 JAVA。

在 JNI 内部,首先会检查调用函数返回的错误代码,之后会调用 ExpertOccurred() 返回一个错误对象。

1
jthrowable ExceptionOccurred(JNIEnv *env);


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 引用类型

jni引用类型.png-38.8kB


改进的 UTF-8 编码

JNI 使用改进的 UTF-8 来表现不同的字符串类型。JAVA 使用 UTF-16 编码。 UTF-8 编码主要适用于 C 语言, 因为他们的编码把 u000 表示为 0xc0,而不是通常的 0x00。 使用改进的字符串可以使得 只包含非空 ASCII 的字符串编码只可以用一个字节(byte)表示。


JNI 函数

JNI 接口不仅有自己的数据集(dataset)也有自己的函数。回顾这些数据集和函数需要花费我们很多时间。可以从官方文档中找到更多信息:

JNI 官方文档入口


JNI 函数使用示例

下面通过简短的例子确保你对这些资料所讲的内容有了正确的理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <jni.h>
...
JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vm_args;
JavaVMOption* options = new JavaVMOption[1];
options[0].optionString = "-Djava.class.path=/usr/lib/java";
vm_args.version = JNI_VERSION_1_6;
vm_args.nOptions = 1;
vm_args.options = options;
vm_args.ignoreUnrecognized = false;
JNI_CreateJavaVM(&jvm, &env, &vm_args);
delete options;
jclass cls = env->FindClass("Main");
jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
env->CallStaticVoidMethod(cls, mid, 100);
jvm->DestroyJavaVM();

大体来看这是一个 C 的代码 然后他应该是从 C 这边调用 JAVA ,我们逐个分析:

  • JavaVM - 提供了一个接口,可以调用函数创建,删除 JAVA 虚拟机.
  • JNIEnv - 确保了大多数的 JNI 函数。
  • JavaVMInitArgs - Java 虚拟机参数
  • JavaVMOption - Java 虚拟机选项

JNI 的 _CreateJavaVM() 方法初始化 JAVA 虚拟机并向 JNI 接口返回一个指针, JNI_DestroyJavaVM() 方法可以载入创建更好的 JAVA 虚拟机。


线程

内核负责管理所有在 Lniux 上运行的线程。线程通过 AttachCurrentThreadAttachCurrentThreadAsDaemon 函数附加到 JAVA 虚拟机。 如果线程没有被添加成功,则不能访问 JNIEnv。Android 系统不能停止 JNI 创建的线程, 即使 GC(Garbage Collection)在运行释放内存时也不行。直到调用 DetachCurrentThread 方法,该线程才会从 JAVA 虚拟机脱离。


作者 @sweatbuffer
2017 年 01月 07日

SweatBuffer wechat
扫描二维码