Android JNI 介绍

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方法中参数可以使用任何类型。包括基本类型,数组类型,复杂对象等。 看下面的例子:

java 复制代码
//包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++层的函数原型为:

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对象:

c 复制代码
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(); //清除例外

c 复制代码
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中也定了几个函数来方便抛出一些常见的例外,如空指针

c 复制代码
//抛出例外"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,以及注册本地方法,例子:

c 复制代码
# 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的定义

c 复制代码
//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 复制代码
/*
 * 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的具体实现:

c 复制代码
/*
 * 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()创建虚拟机。

c 复制代码
// 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文件中:

c 复制代码
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函数。

c 复制代码
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中

c 复制代码
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()函数:

c 复制代码
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文件中

c 复制代码
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

相关推荐
IT毕设梦工厂10 小时前
计算机毕业设计选题推荐-在线拍卖系统-Java/Python项目实战
java·spring boot·python·django·毕业设计·源码·课程设计
工业互联网专业12 小时前
毕业设计选题:基于springboot+vue+uniapp的驾校报名小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
魔术师卡颂20 小时前
如何让“学源码”变得轻松、有意义
前端·面试·源码
Java指南修炼3 天前
一个开源的大语言模型(LLM)服务工具,支持Llama 3.1、Phi 3、Mistral、Gemma 2 等, 87.4k star你必须拥有(附源码)
人工智能·后端·语言模型·开源·源码
一 乐4 天前
英语学习交流平台|基于java的英语学习交流平台系统小程序(源码+数据库+文档)
java·数据库·vue.js·学习·小程序·源码
IT毕设梦工厂5 天前
计算机毕业设计选题推荐-推拿知识互动平台-Java/Python项目实战
java·spring boot·python·django·毕业设计·源码·课程设计
工业互联网专业5 天前
基于ssm+vue+uniapp的智能停车场管理系统小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
IT毕设梦工厂6 天前
计算机毕业设计选题推荐-勤工俭学兼职系统-助学兼职系统-Java/Python项目实战(亮点:手机验证码验证+数据可视化)
java·spring boot·python·django·毕业设计·源码·课程设计
一 乐6 天前
在线小说|基于java的小说阅读系统小程序(源码+数据库+文档)
java·开发语言·数据库·小程序·源码