JNI开发看似简单,但是初学者,通过搜索引擎东拼西凑的资料来写代码,几乎一定会踩坑,比方内存泄露,引用泄漏以及各种崩溃。本来代码只写了一小时,但是解决问题却花了一天。这篇文章的目的就是系统的讲解常见的这些问题,除了官方文档外,让你不再需要参考网上任何一篇文章。
静态注册和动态注册
静态注册
现在在Android Studio上可以直接创建“Native C++”的模版工程。native-lib.cpp使用了静态注册,最终会编译出“libnative-lib.so”,文件内容如下:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_jianshu_qianlang_jnitutorial_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
对应的Java native方法如下:
package com.jianshu.qianlang.jnitutorial;
public class MainActivity extends AppCompatActivity {
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
静态注册之所以能被Java虚拟机通过指定的规则访问到,是因为动态库中导出了指定的符号。JNI中使用了“JNIEXPORT”宏进行符号的导出。
#define JNIIMPORT
#define JNIEXPORT __attribute__ ((visibility ("default")))
#define JNICALL
attribute ((visibility ("default")))即是将当前符号导出。
在NDK的目录下有“arm-linux-androideabi-nm”命令,可以用来查看导出的符号:
toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-nm
“-D” 参数表示 “Display dynamic symbols instead of normal symbols”
arm-linux-androideabi-nm -D libnative-lib.so
命令执行后显示,我们截取片段如下,会看到第一个就是我们要导出的符号 “ Java_com_autonavi_minimap_myapplication_MainActivity_stringFromJNI”:
00008bf8 T Java_com_autonavi_minimap_myapplication_MainActivity_stringFromJNI
00009692 T _ZN10__cxxabiv119__getExceptionClassEPK21_Unwind_Control_Block
0000968c T _ZN10__cxxabiv119__setExceptionClassEP21_Unwind_Control_Blocky
00009698 T _ZN10__cxxabiv121__isOurExceptionClassEPK21_Unwind_Control_Block
00008ca2 W _ZN7_JNIEnv12NewStringUTFEPKc
000124fc T _ZNKSt10bad_typeid4whatEv
0000a7c8 T _ZNKSt11logic_error4whatEv
0000a714 T _ZNKSt13bad_exception4whatEv
...
所以按照指定的规则——“包名+类名+方法名”,在编译期就生成并导出相应的符号,Java虚拟机在执行时便可通过Java方法查找到对应的Native方法,这便是被称为静态注册的原因。
静态注册的函数会在运行时虚拟机会通过dlsym调用。
动态注册
JNI_OnLoad是System.loadLibrary之后加载的第一个方法,所以需要在JNI_OnLoad方法进行动态注册
以下是完整的示意代码:
#include <jni.h>
#include <string>
jstring stringFromJNI(JNIEnv* env, jobject /* this */) {
std::string hello = "Hello from C++ dynamic";
return env->NewStringUTF(hello.c_str());
}
jint RegisterNatives(JNIEnv *env) {
// 反射Java类
jclass clazz = env->FindClass("com/jianshu/qianlang/jnitutorial/MainActivity");
// 如果因修改了包名或者类不存在则反射失败
if (clazz == NULL) {
return JNI_ERR;
}
// 方法数组,分别为:
// 方法名 | 方法签名 | 函数指针
JNINativeMethod methods_MainActivity[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
};
jint nMethods = sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]);
return env->RegisterNatives(clazz, methods_MainActivity, nMethods);
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
// 首先获取JNIEnv
JNIEnv* env = NULL;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
// 动态注册方法
if (RegisterNatives(env) != JNI_OK) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
从 JNI_OnLoad 进行的任何 FindClass 调用都会在用于加载共享库的类加载器上下文中解析类。从其他上下文调用时,FindClass 会使用与 Java 堆栈顶部的方法相关联的类加载器,如果没有(因为调用来自刚刚附加的原生线程),则会使用“系统类加载器”。
由于系统类加载器不知道应用的类,因此您将无法在该上下文中使用 FindClass 查找您自己的类。这使得 JNI_OnLoad 成为了查找和缓存类的便捷位置:一旦有了有效的 jclass,您就可以从任何附加的线程使用它。
两者比较
动态注册可以预先检查符号是否存在,比方要反射的类或者方法不存在时能主动返回错误,而且还可以通过只导出 JNI_OnLoad 来获得规模更小、速度更快的共享库。
静态注册的优势在于要写的代码更少一些。
综上所述推荐大家使用动态注册的方式。
动态注册时的方法签名
动态注册需要获取Java方法的签名,可以使用javap命令。参考:javap 获取JNI方法签名
本专题的其他内容
- JavaVM 和 JNIEnv
- javap 获取JNI方法签名
- JNI静态注册和动态注册
参考
Android Developers - JNI tips
Java Native Interface Specification—Contents
网友评论