Android-JNI开发概论

·

什么是JNI开发 #

JNI的全称是Java Native Interface,顾名思义,这是一种解决Java和C/C++相互调用的编程方式。它其实只解决两个方面的问题,怎么找到和怎么访问。 弄清楚这两个话题,我们就学会了JNI开发。需要注意的是,JNI开发只涉及到一小部分C/C++开发知识,遇到问题的时候我们首先要判断是C/C++的问题还是JNI的问题,这可以节省很多搜索和定位的时间。

用JVM的眼光看函数调用 #

我们知道Java程序是不能单独运行的,它需要运行在JVM上的,而JVM却又需要跑在物理机上,所以它的任务很重,既要处理Java代码,又要处理各种操作系统,硬件等问题。可以说了解了JVM,就了解了Java的全部,当然包括JNI。所以我们先以JVM的身份来看看Java代码是怎样跑起来的吧(只是粗略的内容,省去了很多步骤,为了突出我们在意的部分)。

运行Java代码前,会先启动一个JVM。在JVM启动后,会加载一些必要的类,这些类中包含一个叫主类的类,也就是含有一个静态成员函数,函数签名为public static void main(String[] args)的方法。资源加载完成后,JVM就会调用主类的main方法,开始执行Java代码。随着代码的执行,一个类依赖另一个类,层层依赖,共同完成了程序功能。这就是JVM的大概工作流程,可以说JVM就好比一座大桥,连接着Java大山和native大山。

现在问题来了,在Java程序中,某个类需要通过JNI技术访问JVM以外的东西,那么它需要怎样告诉我(我现在是JVM)呢?需要一种方法 把普通的Java方法标记成特殊,这个标记就是native关键字(使用Kotlin时虽然也可以使用这个关键字,但是Kotlin有自己的关键字external)。当我执行到这个方法时,看到它不一样的标记,我就会从其他地方而不是Class里面寻找执行体,这就是一次JNI调用。也就是说对于Java程序来说,只需要将一个方法标记为native,在需要的地方调用这个方法,就可以完成JNI调用了。但是对于我,该怎样处理这一次JNI调用呢?其实上面的寻找执行体的过程是一个跳转问题,在C/C++的世界,跳转问题就是指针问题。那么这个指针它应该指向哪里呢?

C/C++代码是一个个函数(下文会将Java方法直接用方法简称,而C/C++函数直接用函数简称)组合起来的,每一个函数都是一个指针,这个特性恰好满足我的需要。但是对于我,外面世界那么大,我并不知道从哪里,找什么东西,给我的信息还是不够。为了限定范围,我规定,只有通过System.loadLibrary(“xxx”)加载的函数,我才会查找,其余的我直接罢工(抛错)。这一下子减轻了我的工作量,至少我知道从哪里找了。

确定了范围,下一步就是在这个范围里确定真正的目标了。Java世界里怎样唯一标识一个类呢,有的人会脱口而出——类名,其实不全对,因为类名可能会重名,我们需要全限定的类名,也就是包名加类名,如String的全限定类名就是java.lang.String。但是这和我们查找native的方法有什么联系呢。当然有联系,既然一个全限定的类名是唯一的,那么它的方法也是唯一的,那么假如我规定以这个类的全限定类名加上方法名作为native函数的函数名,这样我是不是就可以通过函数名的方式找到native的函数看呢,答案是肯定的,但是有瑕疵,因为Java系统支持方法重载,也就是一个类里面,同名的方法可能有多个。那么构成重载的条件是什么呢,是参数列表不同。所以,结果就很显然了,我在前面的基础上再加上参数列表,组合成查找条件,我是不是就可以唯一确定某一个native函数了呢,这就是JNI的静态注册。

不过,既然我只需要确定指针的指向,那么我能不能直接给指针赋值,而不是每次都去查找呢,虽然我不知道累,但是还是很耗费时间的。对于这种需求,我当然也是满足的啦,你直接告诉我,我就不找了,我还乐意呢。而且,既然你都给我找到了,我就不需要下那么多规定了,都放开,你说是我就相信你它是。这就是JNI的动态注册。

JNI的函数注册 #

上一节我们通过化身JVM的方式了解了JNI函数注册的渊源,并且引出了两种函数注册方式。从例子上,我们也可以总结出两种注册方式的特点

注册类型优点缺点
静态注册JVM自动查找
实现简单
函数名贼长,限制较多
查找耗时
动态注册运行快
对函数名无限制
实现复杂

那么具体怎么做呢?我们接着往下说。

静态注册 #

虽然静态注册限制比较多,但是都是一些浅显的规则,更容易实施,所以先从静态注册开始讲解。

静态注册有着明确的开发步骤

  1. 编写Java类,声明native方法;
  2. 使用java xxx.java将Java源文件编译为class文件
  3. 使用javah xxx生成对应的.h文件
  4. 构建工具中引入.h文件
  5. 实现.h文件中的函数

上面的这个步骤是静态开发的基本步骤,但是其实在如今强大的IDE面前,这些都不需要我们手动完成了,在Android Studio中,定义好native方法后,在方法上按alt + enter就可以生成正确的函数签名,直接写函数逻辑就可以了。但是学习一门学问,我们还是要抱着求真,求实的态度,所以我用一个例子来阐述一下这些规则,以加深读者的理解。

Test.java

package me.hongui.demo

public class Test{
    native String jniString();
}

native-lib.cpp

#include <jni.h>

extern "C" jstring Java_me_hongui_demo_Test_jniString(JNIEnv *env, jobject thiz) {
    // TODO: implement jniString()
}

上面就是一个JNI函数在两端声明的例子,不难发现

  1. 函数签名以Java_为前缀
  2. 前缀后面跟着类的全路径,也就是包含包名和类名
  3. _作为路径分隔符
  4. 函数的第一个参数永远是JNIEnv *类型,第二个参数根据函数类型的不同而不同,static类型的方法,对应的是jclass类型,否则对应的是jobject类型。类型系统后面会详细展开。

为什么Java方法对应到C/C++函数后,会多两个参数呢。我们知道JVM是多线程的,而我们的JNI方法可以在任何线程调用,那么怎样保证调用前后JVM能找到对应的线程呢,这就是函数第一个参数的作用,它是对线程环境的一种封装,和线程一一对应,也就是说不能用一个线程的JNIEnv对象在另一个线程里使用。另外,它是一个C/C++访问Java世界的窗口,JNI开发的绝大部分时间都是和JNIEnv打交道。

动态注册 #

同样按照开发过程,我们一步一步来完成。 我们把前面的Java_me_hongui_demo_Test_jniString函数名改成jniString(当然不改也可以,毕竟没限制),参数列表保持不变,这时,我们就会发现Java文件报错了,说本地方法未实现。其实我们是实现了的,只是JVM找不到。为了让JVM能找到,我们需要向JVM注册。 那么怎么注册,在哪注册呢,似乎哪里都可以,又似乎都不可以。 前面说过,JVM只会查找通过System.loadLibrary(“xxx”); 加载的库,所以要想使用native方法,首先要先加载包含该方法的库文件,之后,才可使用。加载了库,说明Java程序要开始使用本地方法了。在加载库之后,调用方法之前,理论上都是可以注册方法的,但是时机怎么确定呢,JNI早就给我们安排好了。JVM在把库加载进虚拟机后,会调用函数jint JNI_OnLoad(JavaVM *vm, void *reserved),以确认JNI的版本,版本信息会以返回值的形式传递给JVM,目前可选的值有JNI_VERSION_1_1,JNI_VERSION_1_2,JNI_VERSION_1_4,JNI_VERSION_1_6。假如库没有定义这个函数,那么默认返回的是JNI_VERSION_1_1,库将会加载失败,所以,为了支持最新的特性我们通常返回较高的版本。既然有了这么好的注册时机,那么下一步就是实现注册了。

但事情并没有这么简单。由JNI_OnLoad函数参数列表可知,目前,可供使用的只有JVM,但是查阅JVM的API,我们并没有发现注册的函数——注册函数是写在JNIEnv类里面的。恰巧的是,JVM提供了获取JNIEnv对象的函数。

JVM有多个和JNIEnv相关的函数,在Android开发中,我们需要使用AttachCurrentThread来获取JNIEnv对象,这个函数会返回执行状态,当返回值等于JNI_OK的时候,说明获取成功。有了JNIEnv对象,我们就可以注册函数了。

先来看看注册函数的声明——jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods。返回值不用多说,和AttachCurrentThread一样,指示执行状态。难点在参数上,第一个参数是jclass类型,第二个是JNINativeMethod指针,都是没见过的主。

为什么需要这么多参数呢,JVM不只需要一个函数指针吗。还是唯一性的问题,记得前面的静态注册吗,静态注册用全限定类型和方法,参数列表,返回值的组合确定了函数的唯一性。但是对于动态注册,这些都是未知的,但是又是必须的。为了确定这些值,只能通过其他的方式。jclass就是限定方法的存在范围,获取jclass对象的方式也很简单,使用JNIEnvjclass FindClass(const char* name)函数。参数需要串全限定符的类名,并且把.换成/,也就是类似me/hongui/demo/Test的形式,为啥这样写,后面会单独拿一节出来细说。

第二个和第三个参数组合起来就是常见的数组参数形式。先来看看JNINativeMethod的定义。

typedef struct { 
    char *name; 
    char *signature; 
    void *fnPtr; 

} JNINativeMethod; 

有个编写诀窍,按定义顺序,相关性是从Java端转到C/C++端,怎么理解呢?name是只的Java端对应的native函数的名字,这是纯Java那边的事,Java那边取啥名,这里就是啥名。第二个signature代表函数签名,签名信息由参数列表和返回值组成,形如(I)Ljava/lang/String;,这个签名就是和两边都有关系了。首先Java那边的native方法定义了参数列表和返回值的类型,也就是限定了签名的形式。其次Java的数据类型对应C/C++的转换需要在这里完成,也就是参数列表和返回值要写成C/C++端的形式,这就是和C/C++相关了。最后一个fnPtr由名字也可得知它是一个函数指针,这个函数指针就是纯C/C++的内容了,代表着Java端的native方法在C/C++对应的实现,也就是前文所说的跳转指针的。知道了这些,其实我们还是写不出代码,因为,我们还有JNI的核心没有说到,那就是类型系统。

JNI的类型系统 #

由于涉及到Java和C/C++两个语言体系,JNI的类型系统很乱,但并非无迹可寻。首先需要明确的是,两端都有自己的类型系统,Java里的booleanintString,C/C++的bool,int,string等等,遗憾的是,它们并不一一对应。也就是说C/C++不能识别Java的类型。既然类型不兼容,谈何调用呢。这也就是JNI欲处理的问题。

JNI类型映射 #

为了解决类型不兼容的问题,JNI引入了自己的类型系统,类型系统里定义了和C/C++兼容的类型,并且还对Java到C/C++的类型转换关系做了规定。怎么转换的呢,这里有个表

Java类型C/C++类型描述
booleanjbooleanunsigned 8 bits
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloat32 bits
doublejdouble64 bits
voidvoidN/A

乍一看,没什么特别的,不过就是加了j前缀(除了void),但是,这只是基本类型,我们应该没忘记Java是纯面向对象的语言吧。各种复杂对象才是Java的主战场啊。而对于复杂对象,情况就复杂起来了。我们知道在Java中,任何对象都是Object类的子类。那么我们是否可以把除上面的基本类型以外的所有复杂类型都当作Object类的对象来处理呢,可是可以,但是不方便,像数组,字符串,异常等常用类,假如不做转换使用起来比较繁琐。为了方便我们开发,JNI又将复杂类型分为下面这几种情况

jobject                     (所有的Java对象)
    |
    |--jclass               (java.lang.Class)
    |--jstring              (java.lang.String)
    |--jarray               (数组)
    |     |
    |     |-- jobjectArray  (Object数组)
    |     |-- jbooleanArray (boolean数组)
    |     |-- jbyteArray    (byte数组)
    |     |-- jcharArray    (char数组)
    |     |-- jshortArray   (short数组)
    |     |-- jintArray     (int数组)
    |     |-- jlongArray    (long数组)
    |     |-- jfloatArray   (float数组)
    |     |-- jdoubleArray  (double数组)
    |--jthrowable           (java.lang.Throwable异常)

两个表合起来就是Java端到C/C++的类型转换关系了。也就是说,当我们在Java里声明native代码时,native函数参数和返回值的对应关系,也是C/C++调用Java代码参数传递的对应关系。但是毕竟两套系统还是割裂的,类型系统只定义了兼容方式,并没有定义转换方式,双方的参数还是不能相互识别,所以,JNI又搞了个类型签名,欲处理类型的自动转换问题。

JNI的类型签名 #

类型签名和类类型映射类似,也有对应关系,我们先来看个对应关系表

类型签名Java类型
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
L fully-qualified-class ;fully-qualified-class
[typetype[]
(arg-types)ret-typemethod type

对于基本类型,也很简单,就是取了首字母,除了boolean(首字母被byte占用了),long(字母被用作了符合对象的前缀标识符)。 着重需要注意的是复合类型,也就是某个类的情况。它的签名包含三部分,前缀L,中间是类型的全限定名称,跟上后缀;,三者缺一不可,并且限定符的分隔符要用/替换, 。 注意,类型签名和类型系统不是一个概念。类型通常是纯字符串的,用在函数注册等地方,被JVM使用的。类型系统是和普通类型一样的,可以定义变量,作为参数列表,被用户使用的。 另外,数组对象也有自己的类型签名,也是有着类型前缀[,后面跟着类型的签名。最后的方法类型,也就是接下来我们着重要讲的地方,它也是由三部分组成()和包含在()里面的参数列表,()后面的返回值。这里用到的所有类型,都是指类型签名。

我们来看个例子

long f (int n, String s, boolean[] arr); 

它的类型签名怎么写呢?我们来一步一步分析

  1. 确定它在Java里面的类型,在表中找出对应关系,确定签名形式。
  2. 用步骤1的方法确定它的组成部分的类型。
  3. 将确定好的签名组合在一起

此例是方法类型,对应表中最后一项,所以签名形式为(参数)返回值。该方法有三个参数,我们按照步骤1的方式逐一确定。

  1. int n对应int类型,签名是I;
  2. String s对应String类型,是复合类型,对应表中倒数第三项,所以它的基本签名形式是L全限定名;。而String的全限定名java.lang.String,用/替换,后变成java/lang/String。按步骤3,将它们组合在一起就是Ljava/lang/String;;
  3. boolean[] arr对应数组类型,签名形式是[类型boolean的签名是Z。组合在一起就是[Z;
  4. 最后来看返回值,返回值是long类型,签名形式是J

按照签名形式将这些信息组合起来就是(ILjava/lang/String;[Z)J注意类型签名和签名之间没有任何分割符,也不需要,类型签名是紧密排列的

再看动态注册 #

有了JNI的类型系统的支持,回过头来接着看动态注册的例子,让我们接着完善它。

  1. 用JVM对象获取JNIEnv对象,即auto status=vm->AttachCurrentThread(&jniEnv, nullptr);
  2. 用步骤1获取的JNIEnv对象获取jclass对象,即auto cls=jniEnv->FindClass("me/hongui/demo/Test");
  3. 定义JNINativeMethod数组,即JNINativeMethod methods[]={{"jniString", "()Ljava/lang/String;",reinterpret_cast<void *>(jniString)}};,这里的方法签名可以参看上一节。
  4. 调用JNIEnvRegisterNatives函数。即status=jniEnv->RegisterNatives(cls,methods,sizeof(methods)/sizeof(methods[0]));
  5. 当然,别忘了实现对应的native函数,即这里的jniString——JNINativeMethod的第三个参数。

这五步就是动态注册中JNI_OnLoad函数的实现模板了,主要的变动还是来自jclass的获取参数和JNINativeMethod的签名等,必须做到严格的一一对应。如下面的例子

extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved){
    JNIEnv* jniEnv= nullptr;
    auto status=vm->AttachCurrentThread(&jniEnv, nullptr);
    if(JNI_OK==status){
        JNINativeMethod methods[]={{"jniString", "()Ljava/lang/String;",reinterpret_cast<void *>(jniString)}};
        auto cls=jniEnv->FindClass("me/hongui/demo/Test");
        status=jniEnv->RegisterNatives(cls,methods,sizeof(methods)/sizeof(methods[0]));
        if(JNI_OK==status) {
            return JNI_VERSION_1_6;
        }
    }
    return JNI_VERSION_1_1;
}

在JNI中使用数据 #

前面磨磨唧唧说了这么一大片,其实才讲了一个问题——怎么找到。虽然繁杂,但好在有迹可循,大不了运行奔溃。下面要讲的这个问题就棘手多了,需要一点点耐性和细心。这一部分也可以划分成两个小问题——***访问已知对象的数据,创建新对象。有一点还是要提一下,这里的访问还创建都是针对Java程序而言的,也就是说,对象是存在JVM虚拟机的堆上的,我们的操作都是基于堆对象的操作。***而在C/C++的代码里,操作堆对象的唯一途径就是通过JNIenv提供的方法。所以,这部分其实就是对JNIenv方法的应用讲解。

Java对象的访问 #

在面向对象的世界中,我们说访问对象,通常指两个方面的内容,访问对象的属性、调用对象的方法。这些操作在Java世界中,很好实现,但是在C/C++世界却并非如此。在JNI的类型系统那一节,我们也了解到,Java中的复杂对象在C/C++中都对应着jobject这个类,显然,无论Java世界中,那个对象如何牛逼,在C/C++中都是一视同仁的。为了实现C/C++访问Java的复杂对象,结合访问对象的方式,JNIEnv提供了两大类方法,一类是对应属性的,一类是对应方法的。借助JNIEnv,C/C++就能实现访问对象的目标了。而且它们还有一个较为统一的使用步骤:

  1. 根据要访问的内容准备好对应id(fieldid或者methodid)。
  2. 确定访问的对象和调用数据
  3. 通过JNIEnv的方法调用完成对象访问

可以看出来,这使用步骤和普通面向对象的方式多了一些准备阶段(步骤1,2)。之前提到过,这部分的内容需要的更多的是耐心和细心,不需要多少酷炫的操作,毕竟发挥空间也有限。这具体也体现在上面的步骤1,2。正是这个准备阶段让整个C/C++的代码变得丑陋和脆弱,但是——又不是不能用,是吧。

看一个例子,Java里定义了一个Person类,类定义如下

public class Person(){
    private int age;
    private String name;

    public void setName(String name){
        return this.name=name;
    }
}

现在,我们在C/C++代码里该怎么访问这个类的对象呢。假定需要读取这个对象的age值,设置这个对象的name值。根据上面的步骤,我们有以下步骤

  1. 准备好agefieldid,setNamemethodid。根据JNIEnv的方法,我们可以看到四个相关的,fieldid,methodid各两个,分普通的和静态的。我们这里都是普通的,所以确定的方法是GetFieldIDGetMethodID。第一个参数就是jclass对象,获取方法前面已经说过,即通过JNIEnvFindClass方法,参数是全限定类名,以/替换.。后面两个参数对应Java端的名称和类型签名,age属于field,int的类型签名是IsetName属于method,签名形式是(参数)返回值,这里参数的签名是Ljava/lang/String;,返回值的签名是V,组合起来就是"(Ljava/lang/String;)V"
  2. 假定我们已经有了Person对象obj,通过Java传过来的。
  3. 分别需要调用两个方法,age是整形属性,要获取它的值,对应就需要使用GetIntField方法。setName是返回值为void的方法。所以应该使用CallVoidMethod

通过上面的分析,得出下面的示例代码。

auto cls=jniEnv->FindClass("me/hongui/demo/Person");
auto ageId=jniEnv->GetFieldID(cls,"age","I");
auto nameId=jniEnv->GetMethodID(cls,"setName","(Ljava/lang/String;)V");
jint age=jniEnv->GetIntField(obj,ageId);
auto name=jniEnv->NewStringUTF("张三");
jniEnv->CallVoidMethod(obj,nameId,name);

从上面的分析和示例来看,耐心和细心主要体现在

  1. 对要访问的属性或者方法要耐心确定类型和名称,并且要保持三个步骤中的类型要一一对应。即调用GetFieldID的类型要以GetXXXField的类型保持一致,方法也是一样。
  2. 对属性或方法的静态非静态修饰也要留心,通常静态的都需要使用带有static关键字的方法,普通的则不需要。如GetStaticIntField就是对应获取静态整型属性的值,而GetIntField则是获取普通对象的整型属性值。
  3. 属性相关的设置方法都是类似于SetXField的形式,里面的X代表着具体类型,和前面的类型系统中的类型一一对应,假如是复杂对象,则用Object表示,如SetObjectField。而访问属性只需要将前缀Set换成Get即可。对于静态属性,则是在SetX之间加上固定的Static,即SetStaticIntField这种形式。
  4. 方法调用则是以Call为前缀,后面跟着返回值的类型,形如CallXMethod的形式。这里X代表返回值。如CallVoidMethod就表示调用对象的某个返回值为void类型的方法。同样对应的静态方法则是在CallX之间加上固定的Static,如CallStaticVoidMethod

向Java世界传递数据 #

向Java世界传递数据更需要耐心。因为我们需要不断地构造对象,组合对象,设置属性。而每一种都是上面Java对象的访问的一种形式。

构造Java对象 #

C/C++构造Java对象和调用方法类似。但是,还是有很多值得关注的细节。根据前面的方法,我们构造对象,首先要知道构造方法的id,而得到id,我们需要得到jclass,构造方法的名字和签名。我们知道在Java世界里,构造方法是和类同名的,但是在C/C++里并不是这样,它有着特殊的名字——<init>,注意,这里的<>不能少。***也就是说无论这个类叫什么,它的构造函数的名字都是<init>。***而函数签名的关键点在于返回值,构造方法的返回值都是void也就是对应签名类型V

接前面那个Person类的例子,要怎样构造一个Person对象呢。

  1. 通过JNIEnvFindClass得到就jclass对象。记得将'替换成/
  2. 根据需要得到合适的构造方法的id。我没有定义构造方法,那么编译器会为它提供一个无参的构造方法。也就是函数签名为()V。调用JNIEnvGetMethodID得到id。
  3. 调用JNIEnvNewObject创建对象,记得传递构造参数。我这里不需要传递。

综上分析,这个创建过程类似于如下示例

auto cls=env->FindClass("me/hongui/demo/Person");
auto construct=env->GetMethodID(cls,"<init>","()V");
auto age=env->GetFieldID(cls,"age","I");
auto name=env->GetFieldID(cls,"name","Ljava/lang/String;");
auto p=env->NewObject(cls,construct);
auto nameValue=env->NewStringUTF("张三");
env->SetIntField(p,age,18);
env->SetObjectField(p,name,nameValue);
return p

上面的示例有个有意思的点,其实示例中创建了两个Java对象,一个是Person对象,另一个是String对象。因为在编程中,String出境的概率太大了,所以JNI提供了这个简便方法。同样特殊的还有数组对象的创建。并且因为数组类型不确定,还有多个版本的创建方法,如创建整型数组的方法是NewIntArray。方法签名也很有规律,都是NewXArray的形式,其中X代表数组的类型,这些方法都需要一个参数,即数组大小。既然提到了数组,那么数组的设置方法就不得不提。设置数组元素的值也有对应的方法,形如SetXArrayRegion,如SetIntArrayRegion就是设置整型数组元素的值。和Java世界不同的是,这些方法都是支持同时设置多个值的。整形数组的签名是这样——void SetIntArrayRegion(jintArray array,jsize start, jsize len,const jint* buf)第二个参数代表设置值的开始索引,第三个参数是数目,第四个参数是指向真正值的指针。其余类型都是类似的。

让数据访问更进一步 #

有些时候,我们不是在调用native方法时访问对象,而是在将来的某个时间。这在Java世界很好实现,总能找到合适的类存放这个调用时传递进来的对象引用,在后面使用时直接用就可以了。native世界也是这样吗?从使用流程上是一样的,但是从实现方式上却是很大不同。

Java世界是带有GC的,也就是说,将某个临时对象X传递给某个对象Y之后,X的生命周期被转移到了Y上了,X不会在调用结束后被销毁,而是在Y被回收的时候才会一同回收。这种方式在纯Java的世界里没有问题,但是当我们把这个临时对象X传递给native世界,试图让它以Java世界那样工作时,应用却崩溃了,报错JNI DETECTED ERROR IN APPLICATION: native code passing in reference to invalid stack indirect reference table or invalid reference: 0xxxxx。为什么同样的操作在Java里面可以,在native却不行呢。问题的根源就是Java的GC。GC可以通过各种垃圾检测算法判断某个对象是否需要标记为垃圾。而在native世界,不存在GC,为了不造成内存泄漏,只能采取最严格的策略,默认调用native方法的地方就是使用Java对象的地方。所以在native方法调用的作用域结束后,临时对象就被GC标记为垃圾,后面想再使用,可能已经被回收了。还好,强大的JNIEnv类同样提供了方法让我们改变这种默认策略——NewGlobalRef。对象只需要通过这种方式告诉JVM,它想活得更久一点,JVM在执行垃圾检测的时候就不会把它标记为垃圾,这个对象就会一直存。在,直到调用DeleteGlobalRef这里NewGlobalRefDeleteGlobalRef是一一对应的,而且最好是再不需要对象的时候就调用DeleteGlobalRef释放内存,避免内存泄漏。

总结 #

JNI开发会涉及到Java和C/C++开发的知识,在用C/C++实现JNI时,基本思想就是用C/C++语法写出Java的逻辑,也就是一切为Java服务。JNI开发过程中,主要要处理两个问题,函数注册和数据访问。

函数注册推荐使用动态注册,在JNI_OnLoad函数中使用JNIEnvRegisterNatives注册函数,注意保持Java的native方法和类型签名的一致性,复合类型不要忘记前缀L、后缀;,并将.替换为/

数据访问首先需要确定访问周期,需要在多个地方或者不同时间段访问的对象,记得使用NewGlobalRef阻止对象被回收,当然还要记得DeleteGlobalRef。访问对象需要先拿到相应的id,然后根据访问类型确定访问方法。设置属性通常是SetXField的形式,获取属性值通常是GetXField的形式。调用方法,需要根据返回值的类型确定调用方法,通常是CallXMethod的形式。当然,这些都是针对普通对象的,假如需要访问静态属性或者方法,则是在普通版本的X前面加上Static。这里的所有X都是指代类型,除了基本类型外,其他对象都用Object替换。

在注册函数和访问数据的时候需要时刻关注的就是数据类型。C/C++数据类型除了基本类型外都不能直接传递到Java里,需要通过创建对象的方式传递。一般的创建对象方式NewObject可以创建任何对象,而对于使用频繁的字符串和数组有对应的快速方法NewStringUTFNewXArray。向Java传递字符串和数组,这两个方法少不了。

青山不改,绿水长流,咱们下期见!