1、JNI原理概述
通常为了更加灵活高效地实现计算逻辑,我们一般使用C/C++实现,编译为动态库,并为其设置C接口和C++接口。用C++实现的一个库其实是一个或多个类的简单编译链产物,然后暴露其实现类的构造方法和纯虚接口类。这样就可以通过多态调用到库内部的实现类及其成员方法。进一步地,为了让不同库之间调用兼容,可以将C++接口进一步封装为一组C接口函数,C接口函数编译时不会添加复杂的函数签名,也不支持函数重载,可以方便其他C或C++客户程序调用。C接口函数的封装需要有"extern C{}"标识,以告诉编译器请使用C编译方式编译这些函数。
进一步地,为了方便上层应用调用C/C++库,如Android应用,可以为C++库封装Java接口。JDK中的JNI组件可以方便地实现在Java中调用C++库函数。基本调用原理如下:
- Java客户代码实现和native方法声明属于Java层,使用Java编译器编译。
- JNI接口实现代码和C++库属于C++层,使用G++编译。
这里假定C++类库已经预编译好了,有现成的so库和C接口使用。首先明确一点的就是,我们要为C++库封装一个Java接口,也即在Java层使用C++库暴露的所有函数,那么:
1、第一步,创建一个Java类,并按照C++库的接口函数声明,创建所有的native本地接口函数声明(可以是static的)。
2、第二步,将这些本地接口声明映射为C++ JNI接口声明,这一步是通过Java提供的工具按照既定的映射机制自动生成的。这也就保证了Java层能正确找到C++实现。( javac -h <directory> <source files> )
3、第三步,实现第二步自动生成的C++ JNI接口函数,在这些接口实现中,按需组织调用C++类库的接口函数,以得到需要的结果。所以,这里要注意的一点是,C++ JNI接口函数实现会编译为一个单独的动态库,并且动态链接所使用的C++类库。(这里没有尝试过静态库,按道理应该也是可以的)。此外,在C++ JNI函数实现中,按照类型签名规则,我们可以获取到从Java层传入的参数,也可以返回特定的数据到Java层。
4、第四步,在Java应用层使用 system.loadLibrary("libName.so"); 加载第三步编译生成的JNI so库,即可间接调用C++库函数。
PS:
1、JNI层类型和Java类型的对应关系,基本数据类型只是简单地加了前缀 j ,如 int <=> jint,double <=> jdouble,下面是一些对象类型(包含数组)的映射关系:
2、签名规则对应表
3、String字符串操作
1 // 在jni实现函数中把jstring类型的字符串转换为C风格的字符串,会额外申请内存 2 const char *str = env->GetStringUTFChars(string,0); 3 // 做检查判断 4 if (str == NULL) { return NULL; } 5 // do something 6 7 // 使用完之后释放申请的内存 8 env->ReleaseStringUTFChars(string,str);
- JNI支持将 jstring 转换成UTF编码和Unicode编码两种。因为Java默认使用Unicode编码,而C/C++默认使用UTF编码。所以使用 GetStringUTFChars(jstring string, jboolean* isCopy) 将 jstring 转换成UTF编码的字符串。其中,jstring 类型参数就是我们需要转换的字符串,而 isCopy 参数的值在实际开发中直接赋值为 0 或 NULL 就好了,表示深拷贝。
- 当调用完 GetStringUTFChars 方法时别忘了对返回结果做完全检查。因为 JVM 需要为产生的新字符串分配内存空间,如果分配失败就会返回NULL,并且会抛出 OutOfMemoryError 异常,所以要对 GetStringUTFChars 的返回结果进行判断。
- 当使用完 GetStringUTFChars 返回的字符串时,还不能忘了释放所申请的内存空间。调用 ReleaseStringUTFChars 方法进行释放。
- 除了将 jstring 转换为 C 风格字符串,JNI 还提供了将 C 风格字符串转换为 jstring 类型。通过 NewStringUTF 函数可以将 UTF 编码的 C 风格字符串转换为 jstring 类型,通过 NewString 函数可以将 Unicode 编码的 C 风格字符串转换为 jstring 类型。这个 jstring 类型会自动转换成 Java 支持的 Unicode 编码格式。
- 除了 jstring 和 C 风格字符串的相互转换之外,JNI 还提供了其他的函数:
2、思考
1、目前即使编译Debug版本,调试还是无法进入到 JNI 实现层。有博客说可以通过attach进程可以进入,我尝试并没有成功。
2、JNI 接口传参和返回数据到Java层要注意数据类型匹配,签名要一致,否则会直接崩溃掉。