Android开发中避免不了会用到C/C++代码来提高应用程序的性能和安全性,由于android开发使用Java代码,这就需要Java与C/C++的相互调用。JNI即是链接Java层与C/C++的桥梁。通过JNI我们可以在Java代码中调用C/C++代码,也可在C/C++代码中调用Java代码。下面简述JNI开发中使用到的相关知识。
一. Java中调用native方法
要在Java中调用本地方法,首先要在Java类中使用”native” 关键字声明本地方法,该方法与C/C++编写的JNI本地方法对应。”native”关键字是告知Java编译器,在Java代码中带有该关键字的方法只是声明,具体有C/C++编写实现。native方法中参数可以使用任何类型。包括基本类型,数组类型,复杂对象等。
看下面的例子:
//包com.example
class HelloJNI {
//加载JNI库
static {
System.loadLibrary("hellojni");
}
//声明本地方法
private native void printHello();
public static void main(String args[]) {
HelloJNI jni = new HelloJNI();
//本地方法调用
jni.printHello();
}
}
Java中调用本地方法,还需要加载C/C++实现的运行库。上面例子中,System.loadLibrary(“hellojni”)即是加载本地库。其中参数是字符串,也就是加载的本地库的文件名称的一部分。在linux平台中,本地库的命名规则为lib + 名称 + .so 。即动态库的名称必须以lib开头,中间的名称即是loadLibrary方法传入的参数。如动态库libhellojni.so,加载要写为System.loadLibrary(“hellojni”)。为什么不把名字写全比如”libhellojni.so“。因为java是跨平台的,不同的平台后缀不同,Linux下为.so。window下为.dll,其他平台的命名规则也不相同。这样可以保证java代码在不同的平台中直接运行而不需要再修改代码。
Java中调用native方法也和普通Java方法没有区别。
二. JNI命名规则
在编写Java中定义的native方法对应的C/C++代码时需要遵循一定的命名规范
比如上面的native方法printHello,在C/C++层的函数原型为:
JNIEXPORT void JNICALL Java_com_example_HelloJNI_printHello(JNIEnv*, jobject);
只要按照以上的函数原型规则编写,虚拟就可以把本地库函数和Java本地方法链接在一起。
JNIEXPORT,JNICALL都是JNI的关键字,表示此函数要被JNI调用,函数原型中必须有这2个关键字,JNI才能正常调用函数。这2个关键字其实都是宏定义。
从上可以看到函数名遵循命名规范:Java_类名_本地方法名。通过该规范可以知道该JNI函数与Java的哪个类的哪个本地方法对应。
再来看看函数原型中的参数,第一个参数JNIEnv*是JNI接口的指针,用来调用JNI表中的各种JNI函数(JNI函数提供的基本函数集)。每个线程一个JNIEnv指针。第二个参数是jobject,是JNI提供的Java本地类型,用来在C/C++代码中访问Java对象,此参数中保存着调用本地方法的对象的一个引用。根据上的例子:jni.printHello()。 jni调用了本地方法printHello(),所以jobject中保存着对jni对象的引用。如果本地方法是静态方法,第二个参数类型为jclass。
这个2个参数是默认参数,支持JNI的函数必须包含这2个参数。从第二个参数以后的参数才是本地方法带有的参数。这个例子中,本地方法没有参数,所以只有2个默认参数,没有更多参数。
三. Java类型的在JNI中的表示
Java程序与C/C++函数之间经常需要进行数据交换,2种语言都有自己的数据类型,不能相互使用。为了能够进行数据交换,JNI提供了一套与Java数据类型相对应的Java本地类型,使得本地语言可以使用Java数据类型。在JNI编程时,从Java代码中接收或者传递数据时,只要使用Java本地类型即可。
Java类型和Java本地类型的映射关系:
Java类型 | Java本地类型 | 内存(字节) |
---|---|---|
byte | jbyte | 1 |
short | jshort | 2 |
int | jint | 4 |
long | jlong | 8 |
float | jfloat | 4 |
double | jdouble | 8 |
char | jchar | 2 |
boolean | jboolean | 1 |
void | void |
以上是对应的基本Java类型,在Java还有各种Java类,对象等引用数据类型。在Java本地类型中也提供了对应的引用数据类型,如下。
Java引用类型 | Java本地类型 |
---|---|
类 | jclass |
对象 | jobject |
String | jstring |
Java本地类型jstring对应的Java中的String类型在内存中占用16位,而C语言中的字符串仅占8位,所以在C语言中无法直接使用jstring因此需要将jstring类型的字符串转成C字符串。
转换方法:
将Java字符串对象转成UTF-8字符串(C字符串), 并返回指针
const jbyte* GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy)
*env:JNI接口指针
string:Java字符串对象
*isCopy:用于指示是否复制字符串,为NULL,则表示不复制字符串,返回的指针指向Java字符串的内部存储,在调用ReleaseStringUTFChars()函数时,JNI会释放Java字符串内部存储区域中的C字符串指针。如果参数为非零值,则表示复制字符串,返回的指针指向新分配的内存空间,需要在使用完后手动释放,此时在调用ReleaseStringUTFChars()函数时,JNI会释放通过GetStringUTFChars()函数复制的新的C字符串指针。
在处理完C字符串后,需要使用ReleaseStringUTFChars()函数释放C字符串,以避免内存泄漏。
void ReleaseStringUTFChars(JNIEnv *env, jstring str, const char *chars);
env:JNIEnv* 类型的指针,代表JNI环境。
str:jstring类型的参数,代表要释放的Java字符串。
chars:const char* 类型的参数,代表要释放的C字符串指针。
使用GetStringUTFChars()函数获取的C字符串是以UTF-8编码格式表示的。如果需要使用其他编码格式,可以使用GetStringChars()函数获取Unicode字符数组,然后根据需要进行转换。
另外Java中还有数组类型比如byte[]对应jbyteArrary, char[]对应jcharArray。其实只是jobject通过typedef定义出来的别名。
Java数组 | Java本地类型 |
---|---|
byte[] | jbyteArrary |
short[] | jshortArrary |
int[] | jintArrary |
long[] | jlongArrary |
float[] | jfloatArrary |
double[] | jdoubleArrary |
char[] | jcharArrary |
boolean[] | jbooleanArrary |
四. 参数签名
在JNI中参数签名在非常重要,我们经常需要获取Java方法和字段ID,调用Java方法,访问Java字段等操作。如何在JNI中描述Java方法的参数和返回类型,就需要用到参数签名。通过参数签名JNI可以描述Java方法的参数和返回类型,并将他们转换为C/C++中对应的数据类型,以便在C/C++中处理
以下参数签名和Java参数的对应关系
参数签名 | Java参数 |
---|---|
Z | boolean |
C | char |
B | byte |
I | int |
S | short |
J | long |
F | float |
D | double |
V | void |
复杂类型的参数签名:”L”加全限定类名再加”;”。如:String类的参数签名:”L/java/lang/String;”对应的JNI类型是jstring,其余java复杂类型对应的JNI类型都是jobject类型。
参数签名中数组的的表示方法是在基本类型符号前加符号”[“,比如boolean[]的参数签名为[Z,以此类推。
五. JNI中调用Java
JNI中不可避免要调用到Java代码。JNI提供了一系列方法供开发者使用。
生成Java对象
生成一个java对象可以使用函数NewObject()原型如下:
jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, …);
参数clazz指java类对象(不是类的实例对象),可以通过函数FindClass()得到。
参数methodID是java类的构造方法ID。
返回类型jobject用来表示生成的Java对象。
FindClass()函数原型:
jclass FindClass(const char* name);
参数name是java类的名称,如“L/java/lang/String”
… : 可变参数列表,用于传递构造函数的参数。
例子,生成Date对象:
jclass dateClass = (*env)->FindClass(env, "java/util/Date");
//获得构造函数ID
jmethodID dateConstructor = (*env)->GetMethodID(env, dateClass, "<init>", "()V");
//获得Date对象。java复杂类型的对应的JNI类型都是jobject,所以返回的dateObject是jobject类型的对象
jobject dateObject = (*env)->NewObject(env, dateClass, dateConstructor);
在GetMethodID方法中,第3个参数传的值为””, 它表示构造函数名称。”()V”是构造函数的参数签名,构造函数没有参数则只写”()”,V表示返回类型为void。
调用java类的方法
得到了java对象后,调用java类的方法就简单了。JNIEnv提供了很多调用java类中的方法函数,这些函数根据java方法的返回值来定义。
原型:
void CallVoidMethod(JNIEnv *env, jobject obj, jmethodID methodID, …);
jboolean CallBooleanMethod(JNIEnv *env, jobject obj, jmethodID methodID, …);
jbyte CallByteMethod(JNIEnv *env, jobject obj, jmethodID methodID, …);
jshort CallShortMethod(JNIEnv *env, jobject obj, jmethodID methodID, …);
jint CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID, …);
jlong CallLongMethod(JNIEnv *env, jobject obj, jmethodID methodID, …);
jfloat CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID, …);
jdouble CallDoubleMethod(JNIEnv *env, jobject obj, jmethodID methodID, …);
jobject CallObjectMethod(JNIEnv *env, jobject obj, jmethodID methodID, …);
参数含义:
env:指向JNI环境的指针。
obj:Java对象的引用。
methodID:Java方法的方法ID。
…:可变参数列表,用于传递Java方法的参数。
JNI调用Java方法先要获取方法的jmethodID,传入到对应的方法中,可变列表传入方法的参数签名,需要按照Java方法的参数类型和顺序来传递参数。
获取方法ID函数原型:
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
参数含义:
env:指向JNI环境的指针。
clazz:Java类的引用。
name:Java方法的名称。
sig:Java方法的签名。用于表示Java方法的参数类型和返回值类型。签名的格式为”(参数类型…)返回值类型”,如果public int add(int a, int b)方法,其签名为”(II)I”
在调用Java方法时,需要先获取Java方法的方法ID。当调用的是java静态方法时,也有对应的静态方法原型:
void CallStaticVoidMethod(JNIEnv *env, jclass clazz, jmethodID methodID, …);
jboolean CallStaticBooleanMethod(JNIEnv *env, jclass clazz, jmethodID methodID, …);
jbyte CallStaticByteMethod(JNIEnv *env, jclass clazz, jmethodID methodID, …);
jshort CallStaticShortMethod(JNIEnv *env, jclass clazz, jmethodID methodID, …);
jint CallStaticIntMethod(JNIEnv *env, jclass clazz, jmethodID methodID, …);
jlong CallStaticLongMethod(JNIEnv *env, jclass clazz, jmethodID methodID, …);
jfloat CallStaticFloatMethod(JNIEnv *env, jclass clazz, jmethodID methodID, …);
jdouble CallStaticDoubleMethod(JNIEnv *env, jclass clazz, jmethodID methodID, …);
jobject CallStaticObjectMethod(JNIEnv *env, jclass clazz, jmethodID methodID, …);
参数含义:
env:指向JNI环境的指针。
clazz:Java类的引用。
methodID:Java静态方法的方法ID。
…:可变参数列表,用于传递Java静态方法的参数。
和调用Java方法一样,调用静态方法首先要获取方法的jmethodID。
原型为:
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
存取java类的域变量
JNI也可以对Java类的变量进行存取操作,JNIEnv分别定义了一组函数来读写域变量。
读取变量的原型:
jobject GetObjectField(JNIEnv *env, jobject obj, jfieldID fieldID);
jboolean GetBooleanField(JNIEnv *env, jobject obj, jfieldID fieldID);
jbyte GetByteField(JNIEnv *env, jobject obj, jfieldID fieldID);
jshort GetShortField(JNIEnv *env, jobject obj, jfieldID fieldID);
jint GetIntField(JNIEnv *env, jobject obj, jfieldID fieldID);
jlong GetLongField(JNIEnv *env, jobject obj, jfieldID fieldID);
jfloat GetFloatField(JNIEnv *env, jobject obj, jfieldID fieldID);
jdouble GetDoubleField(JNIEnv *env, jobject obj, jfieldID fieldID);
在调用方法获取变量值之前,需要获取Java类的实例变量的jfieldID。
获取原型为:
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
参数含义:
env:指向JNI环境的指针。
clazz:Java类的引用。
name:Java变量的名称。
sig:Java变量的签名。
读取静态变量的函数原型:
jobject GetStaticObjectField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jboolean GetStaticBooleanField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jbyte GetStaticByteField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jshort GetStaticShortField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jint GetStaticIntField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jlong GetStaticLongField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jfloat GetStaticFloatField(JNIEnv *env, jclass clazz, jfieldID fieldID);
jdouble GetStaticDoubleField(JNIEnv *env, jclass clazz, jfieldID fieldID);
获取Java类的静态变量的变量ID,函数原型为:
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
参数含义:
env:指向JNI环境的指针。
clazz:Java类的引用。
name:Java变量的名称。
sig:Java变量的签名。
设置变量的函数原型:
void SetObjectField(JNIEnv *env, jobject obj, jfieldID fieldID, jobject value);
void SetBooleanField(JNIEnv *env, jobject obj, jfieldID fieldID, jboolean value);
void SetByteField(JNIEnv *env, jobject obj, jfieldID fieldID, jbyte value);
void SetShortField(JNIEnv *env, jobject obj, jfieldID fieldID, jshort value);
void SetIntField(JNIEnv *env, jobject obj, jfieldID fieldID, jint value);
void SetLongField(JNIEnv *env, jobject obj, jfieldID fieldID, jlong value);
void SetFloatField(JNIEnv *env, jobject obj, jfieldID fieldID, jfloat value);
void SetDoubleField(JNIEnv *env, jobject obj, jfieldID fieldID, jdouble value);
设置静态变量的函数原型:
void SetStaticObjectField(JNIEnv *env, jclass clazz, jfieldID fieldID, jobject value);
void SetStaticBooleanField(JNIEnv *env, jclass clazz, jfieldID fieldID, jboolean value);
void SetStaticByteField(JNIEnv *env, jclass clazz, jfieldID fieldID, jbyte value);
void SetStaticShortField(JNIEnv *env, jclass clazz, jfieldID fieldID, jshort value);
void SetStaticIntField(JNIEnv *env, jclass clazz, jfieldID fieldID, jint value);
void SetStaticLongField(JNIEnv *env, jclass clazz, jfieldID fieldID, jlong value);
void SetStaticFloatField(JNIEnv *env, jclass clazz, jfieldID fieldID, jfloat value);
void SetStaticDoubleField(JNIEnv *env, jclass clazz, jfieldID fieldID, jdouble value);
六. JNI的异常处理
在JNI中检查Java层中发生的例外
android的C++层不支持try-catch机制。如果JNI中调用java层的方法发生了例外,则JNI调用会正常返回,但是继续调用其他JNI函数可能会导致崩溃。所以JNI中提供了一组函数来检查java方法是否抛出了例外。
jthrowable ExceptionOccurred(); //检查是否有例外发生
void ExceptionDescribe(); //打印输出Exception的信息
void ExceptionClear(); //清除例外
jclass clazz = env->FindClass("android/content/Intent");
if (env->ExceptionOccurred() != NULL) {
env->ExceptionDescribe();
env->ExceptionClear();
}
在JNI中抛出例外
如果要在JNI中抛出例外可以使用以下函数:
jint ThrowNew(jclass clazz, const char* message) //用来新生成一个例外并向外抛出,参数clazz 是指java中的Exception类以及其派生类的类对象
jint jniThrowException(JNIEnv *env, const char *className, const char *msg);
jniThrowException()函数是在JNI代码中抛出Java异常的一种方式。它可以抛出预定义的Java异常,如IllegalArgumentException、IllegalStateException等。ThrowNew()函数可以在本地代码中灵活地抛出任何Java异常,而不仅仅是预定义的异常。预定义异常是指,这些异常类已经在Java标准库中定义好了的,而不是开发者自定义的异常类。
在JNIHelp中也定了几个函数来方便抛出一些常见的例外,如空指针
//抛出例外“java/lang/NullPointerException”
//参数message: 异常消息。可以为NULL。
void jniThrowNullPointerException(JNIEnv *env, const char *message);
//抛出例外“java/lang/RuntimeException”
//参数message: 异常消息。可以为NULL。
void jniThrowRuntimeException(JNIEnv *env, const char *message);
//抛出例外“java/lang/IOException”
//参数errnum:错误码。必须是一个有效的系统错误码。
void jniThrowIOException(JNIEnv *env, jint errnum);
抛出异常后,函数会立即返回,不再执行后续代码。因此,调用该函数后的代码应该不依赖于函数的返回值,而是依赖于异常处理机制来处理异常。
七. JNI中的引用
JNI中创建的java对象和java层创建的对象并没有区别,它们的生命周期是一致的。
下面介绍JNI中对象的引用类型。
局部引用(LocalReference)
局部引用在JNI本地函数中生成,他们的生命周期应该在函数退出时结束。当本地方法返回时,所有局部引用都会被自动释放。局部引用是JNI默认的,也可以使用NewLocalRef()函数创建。虚拟机如何做到函数执行期不回收,退出函数就回收?每个java线程中都有一张本地引用(LocalReference)表,虚拟机不回收这张表里的对象。本地函数执行期间,JNIEnv隐式地把java对象都加入到本地引用表中了。(注意:如果本地函数生成的java对象希望留给下次调用使用,不能把它保存在一个本地全局静态变量中,这样并不能阻止它被回收掉)。
全局引用(GlobalReference)
全局引用可以在本地方法中创建,但是在本地方法返回后,不会被回收。JNIEnv提供了一个函数NewGlobalRef()函数:jobject NewGlobalRef(JNIEnv* env, jobject obj);这个函数的作用是将java对象从本地引用表中删除掉,然后放入全局引用表中。放入全局引用表中的java对象不会被回收。全局引用对象需要显式删除,原型为:void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
该函数把对象从全局引用表中删除,不立刻回收对象,等下次系统垃圾回收时才会真正释放。
弱全局引用(WeakGlobalReference)
弱全局引用与全局引用类似,但它只是暂时保存对象,下次垃圾回收时将会被回收,原型:
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj); //创建弱全局引用
void DeleteWeakGlobalRef(JNIEnv *env, jweak weakRef); // 删除弱全局引用
八. JNI函数注册
虚拟机在运行包含本地方法的java应用程序时,经历一下步骤。
1.调用System.loadLibrary() 方法,将包含本地方法具体实现的C/C++运行库加载到内存中
2.虚拟机检索加载进来的库函数符号,在其中查找与java本地方法拥有相同签名的JNI本地函数符号,如找到一致的,则将本地方法映射到具体的JNI本地函数。
如果JNI本地函数只有少数几个,Java虚拟机在将本地方法与C运行库中的JNI本地函数映射在一起,不会耗费太长时间。但在Android Framework复杂的系统下,有大量的包含本地方法的Java类。虚拟机加载相应运行库,再逐一检索,将各个本地方法与相应的函数映射起来,显然会增加运行时间,降低效率。为了解决这一问题,JNI提供了名称为RegisterNatives()的JNI函数,该函数允许C/C++开发者将JNI本地函数与java类的本地方法直接映射在一起。当不调用RegisterNatives()函数时,Java虚拟机会自动检索并将JNI本地函数与相应的Java本地方法链接在一起。当开发者直接调用RegisterNatives()函数进行隐射时,Java虚拟机就不必进行映射处理,这会极大提高运行速度,提升运行效率。由于是开发直接将JNI本地函数与Java本地方法链接在一起,在加载运行库时虚拟机不必为了识别JNI本地函数将JNI本地函数的名称与JNI支持的命名规则进行对比,即任何名称的函数都能直接连接到Java本地方法上。
再看看System.loadLibrary()方法的执行过程,在调用System.loadLibrary()时,首先虚拟机加载其参数指定的共享库,并检索共享库内的函数符号,检查JNI_OnLoad()函数是否被实现,若共享库中含有相关函数,则JNI_OnLoad()函数会被自动调用。如果未被实现,虚拟机会自动将本地方法与库内的JNI本地函数符号进行比较匹配。
如果,开发者想手工映射本地方法与JNI本地函数,需要实现JNI_OnLoad()函数,并在JNI_OnLoad()函数内部调用RegisterNaives()函数进行映射匹配。
JNI_OnLoad()函数必须返回有关JNI版本的信息,通过JNI_OnLoad()函数,虚拟机可以在加载本地库时对JNI进行初始化。
JNI_OnLoad()函数:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
vm:指向Java虚拟机的指针。该指针可以用来与Java虚拟机进行交互,如获取当前线程、获取Class对象等。
reserved:指向一个保留参数的指针。该参数保留,目前没有使用。
返回值:是一个整数值,用于指示JNI版本号
在使用加载库的过程中,JNI_OnLoad()函数会向虚拟机确认JNI的版本。如果库中不包含JNI_OnLoad()函数, 虚拟机会认为相关库要求JNI1.1版本支持。
特别注意:在JNI_OnLoad()函数中,不应该调用Java方法,因为此时Java虚拟机还没有完全初始化完成。
RegisterNatives函数原型:
JNIEXPORT jint JNICALL RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
函数参数:
env:指向JNI环境的指针。
clazz:要注册本地方法的Java类对象。
methods:指向一个JNINativeMethod结构体数组的指针,其中包含要注册的本地方法的信息,包括方法名、方法签名和本地方法的函数指针。
nMethods:要注册的本地方法的数量。
如何实现JNI_OnLoad,以及注册本地方法,例子:
# include " jni . h "
# include < stdio.h >
// JNI 本地函数原型
// RegisterNatives函数进行映射时,不需要将JNI本地函数原型与JNI命名规则进行比较,但是函数中的2个公共参数必须指定为"JNIEnv * env , jobjectobj"
void printHelloNative ( JNIEnv * env , jobjectobj );
void printStringNative ( JNIEnv * env , jobject obj , jstring string ); JNIEXPORT jint JNICALL JNI_OnLoad ( JavaVM * vm , void *reserved )
{
JNIEnv *env = NULL;
JNINativeMethod nm [2];
jclass cls;
jint result = -1;
if ( vm->GetEnv (( void **) & env, JNI_VERSION_1_4) != JNI _ OK )
{
printf ("Error");
return JNI_ERR;
}
// 为了把上面声明的JNI本地函数与HelloJNI类的本地方法链接到一起。本行先调用FindClass()函数加载HelloJNI类,并将类引用保存到jclass变量cls中。
cls = env -> FindClass ("HelloJNI ");
//以下代码用来将Java类的本地方法与JNI本地函数映射在一起。使用JNINativeMethod结构体数组,将待映射的本地方法与JNI本地函数的相关信息保存在数组中。
nm[O].name = "printHello";
nm[0].signature = "()V";
nm[0].fnPtr = ( void *) printHelloNative ;
nm[1].name = "printString";
nm[1].signature = "(Ljava/lang/String;)V ";
nm[1].fnPtr = ( void *)printStringNative;
//数组传给RegisterNatives 完成映射
env -> RegisterNatives ( cls , nm , 2);
return JNI_VERSION_1_4;
}
//实现 JNI 本地函数
void printHelloNative ( JNIEnv *env , jobject obj )
{
printf (" HelloWorld !\ n ");
return ;
}
void printStringNative ( JNIEnv *env , jobject obj , jstring string )
{
const char *str = env -> GetStringUTFChars ( string ,0);
printf ("%s!\n", str );
return ;
}
//JNINativeMethod结构体
typedef struct {
char *name; //本地方法名
char *signature; // 本地方法签名
void *fnPtr; //与本地方法相应的JNI本地函数指针
} JNINativeMethod
另外也可以直接映射本地方法,不使用JNI_Onload()函数,而在C中直接调用RegisterNatives函数,完成JNI本地函数与Java类本地方法间的映射。Android的app_process系统进程即采用这种方法,实现Android Framework的各种Java类本地方法与C函数映射。具体源码不在赘述,有兴趣可以自己研究。
九. JNI环境创建
在JNI本地函数开发过程中,我们经常可以看到一个重要的结构体JNIEnv,它是代表JNI环境的结构体。通过这个结构体我们才能调用到JNI为我们提供的丰富的API。
系统源码libnativehelper/include_jni/jni.h文件中定了很多JNI相关的结构体,其中可以看到JNIEnv的定义
//jni.h头文件
...
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
...
上面头文件代码可以看到,JNI的定义区分了C和C++,android中已经定义了宏 __cplusplus所以关注C++部分即可。C++部分可以看出JNIEnv等同于_JNIEnv结构体。_JNIEnv的定义也在jni.h头文件中
/*
* C++ object wrapper.
*
* This is usually overlaid on a C struct whose first element is a
* JNINativeInterface*. We rely somewhat on compiler behavior.
*/
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
jint GetVersion()
{ return functions->GetVersion(this); }
jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
jsize bufLen)
{ return functions->DefineClass(this, name, loader, buf, bufLen); }
jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }
...
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }
jobject GetObjectField(jobject obj, jfieldID fieldID)
{ return functions->GetObjectField(this, obj, fieldID); }
jboolean GetBooleanField(jobject obj, jfieldID fieldID)
{ return functions->GetBooleanField(this, obj, fieldID); }
jbyte GetByteField(jobject obj, jfieldID fieldID)
{ return functions->GetByteField(this, obj, fieldID); }
jchar GetCharField(jobject obj, jfieldID fieldID)
{ return functions->GetCharField(this, obj, fieldID); }
jshort GetShortField(jobject obj, jfieldID fieldID)
{ return functions->GetShortField(this, obj, fieldID); }
jint GetIntField(jobject obj, jfieldID fieldID)
{ return functions->GetIntField(this, obj, fieldID); }
jlong GetLongField(jobject obj, jfieldID fieldID)
{ return functions->GetLongField(this, obj, fieldID); }
jfloat GetFloatField(jobject obj, jfieldID fieldID)
{ return functions->GetFloatField(this, obj, fieldID); }
jdouble GetDoubleField(jobject obj, jfieldID fieldID)
{ return functions->GetDoubleField(this, obj, fieldID); }
....
在_JNIEnv结构体中我们可以看到有个JNINativeInterface结构体的指针变量functions,开发中使用的各种JNI函数,比如GetFieldID, GetBooleanField等等,都是调用JNINativeInterface的函数。实际上_JNIEnv是JNINativeInterface的包装类。
JNINativeInterface的具体实现:
/*
* Table of interface function pointers.
*/
struct JNINativeInterface {
void* reserved0;
void* reserved1;
void* reserved2;
void* reserved3;
jint (*GetVersion)(JNIEnv *);
jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
jsize);
jclass (*FindClass)(JNIEnv*, const char*);
...
jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
jobject (*CallObjectMethodV)(JNIEnv*, jobject, jmethodID, va_list);
jobject (*CallObjectMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);
jboolean (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...);
jboolean (*CallBooleanMethodV)(JNIEnv*, jobject, jmethodID, va_list);
jboolean (*CallBooleanMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);
....
JNINativeInterface中定义的都是函数指针,这些函数指针在哪里初始化的?
JNIEnv和线程相关,每个线程都有自己的JNIEnv对象。主线程的JNIEnv是在Zygote进程创建过程中创建的。Zygote进程中会调用函数JNI_CreateJavaVM()创建虚拟机。
// JNI Invocation interface.
extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
...
*p_env = Thread::Current()->GetJniEnv();
*p_vm = runtime->GetJavaVM();
return JNI_OK;
}
//thread.h
// JNI methods
JNIEnvExt* GetJniEnv() const {
return tlsPtr_.jni_env;
}
// thread.cc
bool Thread::Init(ThreadList* thread_list, JavaVMExt* java_vm, JNIEnvExt* jni_env_ext) {
...
if (jni_env_ext != nullptr) {
DCHECK_EQ(jni_env_ext->GetVm(), java_vm);
DCHECK_EQ(jni_env_ext->GetSelf(), this);
tlsPtr_.jni_env = jni_env_ext;
} else {
std::string error_msg;
tlsPtr_.jni_env = JNIEnvExt::Create(this, java_vm, &error_msg);
if (tlsPtr_.jni_env == nullptr) {
LOG(ERROR) << "Failed to create JNIEnvExt: " << error_msg;
return false;
}
}
ScopedTrace trace3("ThreadList::Register");
thread_list->Register(this);
return true;
}
JNI_CreateJavaVM函数中调用Thread::Current()->GetJniEnv()创建JNIEnv,GetJniEnv函数定义在jni.h头文件中,根据源码可以看到,它直接returnt tlsPtr_.jni_env。而tlsPtr_.jni_env的创建是在Thread::Init函数中,通过JNIEnvExt::Create函数获取。这样主线程的JNIEnv环境即创建成功。
再看看其他线程的JNIEnv创建过程。Java中创建新线程调用Thread.start()方法。start调用native方法nativeCreate(),对应的本地实现Thread_nativeCreate()位于art\runtime\native下的java_lang_Thread.cc文件中:
static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size,
jboolean daemon) {
// There are sections in the zygote that forbid thread creation.
Runtime* runtime = Runtime::Current();
if (runtime->IsZygote() && runtime->IsZygoteNoThreadSection()) {
jclass internal_error = env->FindClass("java/lang/InternalError");
CHECK(internal_error != nullptr);
env->ThrowNew(internal_error, "Cannot create threads in zygote");
return;
}
Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}
//Thread::CreateNativeThread 函数
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
...
pthread_create_result = pthread_create(&new_pthread,
&attr,
gUseUserfaultfd ? Thread::CreateCallbackWithUffdGc : Thread::CreateCallback, child_thread);
// Thread::CreateCallback 函数
void* Thread::CreateCallback(void* arg) {
Thread* self = reinterpret_cast<Thread*>(arg);
Runtime* runtime = Runtime::Current();
if (runtime == nullptr) {
LOG(ERROR) << "Thread attaching to non-existent runtime: " << *self;
return nullptr;
}
{
// TODO: pass self to MutexLock - requires self to equal Thread::Current(), which is only true
// after self->Init().
MutexLock mu(nullptr, *Locks::runtime_shutdown_lock_);
// Check that if we got here we cannot be shutting down (as shutdown should never have started
// while threads are being born).
CHECK(!runtime->IsShuttingDownLocked());
// Note: given that the JNIEnv is created in the parent thread, the only failure point here is
// a mess in InitStackHwm. We do not have a reasonable way to recover from that, so abort
// the runtime in such a case. In case this ever changes, we need to make sure here to
// delete the tmp_jni_env, as we own it at this point.
//调用Thread Init函数
CHECK(self->Init(runtime->GetThreadList(), runtime->GetJavaVM(), self->tlsPtr_.tmp_jni_env));
...
}
Thread_nativeCreate函数调用Thread::CreateNativeThread函数,在CreateNativeThread函数中,调用pthread_create来创建一个新线程,新线程的运行函数是CreateCallback。在CreateCallback中可以看到调用了self->Init,而这个self即为当前线程实例。Thread::Init上面已经分析,在Init函数中创建了JNIEnv对象。可以看到这个和主线程的创建过程是一致的。
上面的代码可以注意到tlsPtr_.jni_env类型是JNIEnvExt,这个JNIEnvExt和JNIEnv是什么关系呢?JNIEnvExt定义在jni_env_ext.h头文件中,打开art/runtime/jni/jni_env_ext.h头文件可以看到,其实JNIEnvExt是继承了JNIEnv类,其中定义了Create函数。
class JNIEnvExt : public JNIEnv {
public:
// Creates a new JNIEnvExt. Returns null on error, in which case error_msg
// will contain a description of the error.
static JNIEnvExt* Create(Thread* self, JavaVMExt* vm, std::string* error_msg);
...
}
创建JNIEnv的函数JNIEnvExt* Create的实现是在thread.cc中
JNIEnvExt* JNIEnvExt::Create(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg) {
std::unique_ptr<JNIEnvExt> ret(new JNIEnvExt(self_in, vm_in));
if (!ret->Initialize(error_msg)) {
return nullptr;
}
return ret.release();
}
JNIEnvExt::JNIEnvExt(Thread* self_in, JavaVMExt* vm_in)
: self_(self_in),
vm_(vm_in),
local_ref_cookie_(jni::kLRTFirstSegment),
locals_(vm_in->IsCheckJniEnabled()),
monitors_("monitors", kMonitorsInitial, kMonitorsMax),
critical_(0),
check_jni_(false),
runtime_deleted_(false) {
MutexLock mu(Thread::Current(), *Locks::jni_function_table_lock_);
check_jni_ = vm_in->IsCheckJniEnabled();
functions = GetFunctionTable(check_jni_);
unchecked_functions_ = GetJniNativeInterface();
}
//GetFunctionTable
const JNINativeInterface* JNIEnvExt::GetFunctionTable(bool check_jni) {
const JNINativeInterface* override = JNIEnvExt::table_override_;
if (override != nullptr) {
return override;
}
return check_jni ? GetCheckJniNativeInterface() : GetJniNativeInterface();
}
在JNIEnvExt::Create函数中new了一个JNIEnvExt对象。而在JNIEnvExt构造函数中functions通过GetFunctionTable(check_jni_)函数获得。functions变量在上面的介绍中知道它是JNINativeInterface*类型的指针。GetFunctionTable函数中,如果check_jni为true,调用GetCheckJniNativeInterface()并返回,如果是false则GetJniNativeInterface()并返回。check_jni用于JNI检查,用于调试发现JNI问题。看看GetJniNativeInterface()函数:
const JNINativeInterface* GetJniNativeInterface() {
// The template argument is passed down through the Encode/DecodeArtMethod/Field calls so if
// JniIdType is kPointer the calls will be a simple cast with no branches. This ensures that
// the normal case is still fast.
return Runtime::Current()->GetJniIdType() == JniIdType::kPointer
? &JniNativeInterfaceFunctions<false>::gJniNativeInterface
: &JniNativeInterfaceFunctions<true>::gJniNativeInterface;
}
//gJniNativeInterface变量定义
template<bool kEnableIndexIds>
struct JniNativeInterfaceFunctions {
using JNIImpl = JNI<kEnableIndexIds>;
static constexpr JNINativeInterface gJniNativeInterface = {
nullptr, // reserved0.
nullptr, // reserved1.
nullptr, // reserved2.
nullptr, // reserved3.
JNIImpl::GetVersion,
JNIImpl::DefineClass,
JNIImpl::FindClass,
JNIImpl::FromReflectedMethod,
JNIImpl::FromReflectedField,
JNIImpl::ToReflectedMethod,
JNIImpl::GetSuperclass,
JNIImpl::IsAssignableFrom,
JNIImpl::ToReflectedField,
JNIImpl::Throw,
JNIImpl::ThrowNew,
...
GetJniNativeInterface()函数定义在jni_internal.cc中,其返回gJniNativeInterface变量。gJniNativeInterface对应的是JNINativeInterface结构体。JNIImpl是JNI的别名,可以看到具体代码是实现是在JNI类中。JNI类也是在jni_internal.cc文件中
template <bool kEnableIndexIds>
class JNI {
public:
static jint GetVersion(JNIEnv*) {
return JNI_VERSION_1_6;
}
static jclass DefineClass(JNIEnv*, const char*, jobject, const jbyte*, jsize) {
LOG(WARNING) << "JNI DefineClass is not supported";
return nullptr;
}
static jclass FindClass(JNIEnv* env, const char* name) {
CHECK_NON_NULL_ARGUMENT(name);
Runtime* runtime = Runtime::Current();
ClassLinker* class_linker = runtime->GetClassLinker();
std::string descriptor(NormalizeJniClassDescriptor(name));
ScopedObjectAccess soa(env);
ObjPtr<mirror::Class> c = nullptr;
if (runtime->IsStarted()) {
StackHandleScope<1> hs(soa.Self());
Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader<kEnableIndexIds>(soa)));
c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);
} else {
c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());
}
return soa.AddLocalReference<jclass>(c);
}
...
这就解开了上面提的问题“JNINativeInterface中定义的都是函数指针,这些函数指针在哪里初始化的?”这个问题,实际的实现类是JNI类。
到这里,终于搞清了JNIEnv的创建和初始化流程。总结起来比较简单,创建线程后在线程的Init方法中创建并初始化JNIEnv环境变量,最终JNINativeInterface的函数指针是在JNI类中实现。
十. 源码路径
JNINativeInterface:libnativehelper/include_jni/jni.h
JNIEnvExt:art/runtime/jni/jni_env_ext.h
art/runtime/jni/jni_env_ext.cc
thread.cc: art/runtime/thread.cc
GetCheckJniNativeInterface(): art/runtime/jni/check_jni.cc
GetJniNativeInterface(): art/runtime/jni/jni_internal.cc
JNI:art/runtime/jni/jni_internal.cc