赞
踩
JNI全称是Java Native Interface,为Java本地接口,是连接Java层与Native层的桥梁。在Android进行JNI开发时,可能会遇到couldn't find "xxx.so"问题,或者内存泄漏问题,或者令人头疼的JNI底层崩溃问题。Java层如何调用Native方法?Java方法的参数如何传递给Native层?而Native层又如何反射调用Java方法?这些问题在本文将得到答案,带着问题去阅读会事半功倍,接下来我们开始全方位介绍与最佳代码实践。
关于ndk编译脚本:Android.mk or CMake
关于JNI开发规范:JNI开发的最佳Tips
目录
在Android提供System.loadLibrary()或者System.load()来加载库。示例如下:
- static {
- try {
- System.loadLibrary("hello");
- } catch (UnsatisfiedLinkError error) {
- Log.e(TAG, "load library error=" + error.getMessage());
- }
- }
需要注意的是,如果.so动态库或.a静态库不存在时,会抛出couldn't find "libxxx.so"异常:
- load library error=dalvik.system.PathClassLoader[DexPathList[[
- zip file "/data/app/com.frank.ffmpeg/base.apk"],
- nativeLibraryDirectories=[/data/app/com.frank.ffmpeg/lib/arm64,
- data/app/com.frank.ffmpeg/base.apk!/lib/arm64-v8a,/system/lib64, /vendor/lib64, /product/lib64]]]
- couldn't find "libhello.so"
如果期待加载的是64bit的库,却加载到32bit的,会报错如下:
java.lang.UnsatisfiedLinkError: dlopen failed: "xxx.so" is 32-bit instead of 64-bit
System.loadLibrary()内部调用Runtime.getRuntime().loadLibrary0(),源码如下:
- synchronized void loadLibrary0(ClassLoader loader, String libraryName) {
- if (loader != null) {
- // 1、调用classLoader查找库
- String filename = loader.findLibrary(libraryName);
- if (filename == null) {
- throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
- System.mapLibraryName(libraryName) + "\"");
- }
- // 2、调用native方法来加载
- String error = nativeLoad(filename, loader);
- if (error != null) {
- throw new UnsatisfiedLinkError(error);
- }
- return;
- }
- // 3、拼接完整库名,比如由hello拼接成libhello.so
- String filename = System.mapLibraryName(libraryName);
- List<String> candidates = new ArrayList<String>();
- String lastError = null;
- for (String directory : getLibPaths()) {
- String candidate = directory + filename;
- candidates.add(candidate);
- if (IoUtils.canOpenReadOnly(candidate)) {
- // 4、调用native方法来加载
- String error = nativeLoad(candidate, loader);
- if (error == null) {
- return; // 加载library成功
- }
- lastError = error;
- }
- }
-
- if (lastError != null) {
- throw new UnsatisfiedLinkError(lastError);
- }
- throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
这里的nativeLoad()属于runtime底层的jni方法,接着调用art/runtime/java_vm_ext.cc的load_NativeLibrary(),最终调用dlopen()来打开so库或a库。 调用过程如下图:
java层调用带native关键字的JNI方法,需要注册java层与native层的对应关系,有静态注册和动态注册两种方式。静态注册一般是应用层使用,绑定包名+类名+方法名,在调用JNI方法时,通过类加载器查找对应的函数。动态注册一般是framework层使用,在JNI_OnLoad()回调时,把JNINativeMethod注册到函数表。静态注册的缺点是包名、类名或方法名发生修改时,native层的jni方法名也得对应修改。
以java层声明的函数名为hello的JNI方法为例:
private native void hello(int num);
静态注册的示例(如果是c++文件(.cpp/.cc/.cxx),需要加extern "C"关键字):
- #ifdef __cplusplus
- extern "C" {
- #endif
- JNIEXPORT void JNICALL Java_com_frank_ffmpeg_FFmpegCmd_hello(JNIEnv *env, jclass thiz, jint num) {
-
- }
- #ifdef __cplusplus
- }
- #endif
如果觉得每个JNI方法都这样写比较麻烦,我们可以写个宏定义:
- #define FFMPEG_FUNC(RETURN_TYPE, FUNC_NAME, ...) \
- JNIEXPORT RETURN_TYPE JNICALL Java_com_frank_ffmpeg_FFmpegCmd_ ## FUNC_NAME \
- (JNIEnv *env, jclass thiz, ##__VA_ARGS__)\
动态注册的示例:
- JNINativeMethod nativeMethods[] {
- {"hello", "(I)V", (void *)"native_hello"},
- {"world", "(J)V", (void *)"native_world"}
- };
-
- jint JNI_OnLoad(JavaVM* vm, void* reserved) {
- JNIEnv *env = NULL;
- vm->GetEnv((void **)&env, JNI_VERSION_1_6);
- jclass clazz = env->FindClass("com/frank/ffmpeg/handler/FFmpegHandler");
- int numMethods = sizeof(nativeMethods) / sizeof(nativeMethods[0]);
- // 注册本地方法到函数表
- env->RegisterNatives(clazz, nativeMethods, numMethods);
- env->DeleteLocalRef(clazz);
- return JNI_VERSION_1_6;
- }
JNINativeMethod的结构体位于jni.h,定义如下:
- typedef struct {
- const char* name;
- const char* signature;
- void* fnPtr;
- } JNINativeMethod;
JNI方法前两个参数分别是JNIEnv和jclass,其中JNIEnv是上下文环境,而jclass是类的实例对象。其他参数为带j开头,比如jint、jstring。
JNI提供局部引用和全局引用,还有全局弱引用。顾名思义,局部引用的作用域为局部,在本地方法返回时被GC主动回收,通过如下方法创建:
jobject NewLocalRef(JNIEnv *env, jobject ref);
全局引用的作用域为全局,不会被GC回收,需要手动释放引用资源,否则导致内存泄漏。全局引用的创建与释放如下:
- // new global reference
- jobject NewGlobalRef(JNIEnv *env, jobject obj);
- // delete global reference
- void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
全局弱引用与全局引用不同的是,它可以被GC回收。另外,它关联到虚引用,用于感知何时被GC回收。全局弱引用的创建与释放如下:
- // new weak global reference
- jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
- // delete weak global reference
- void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
JNI提供检测异常、抛出异常和清除异常。使用ExceptionOccurred()进行异常检测。在检测到异常后通过ThrowNew()抛出异常,方法如下:
jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);
最后是清除异常,使用ExceptionClear()。完整的示例代码如下:
- // 检测异常
- if (env->ExceptionOccurred() != NULL) {
- // 抛出异常
- jclass clazz = env->FindClass("java/lang/NullPointerException");
- env->ThrowNew(clazz, "This is a null pointer...");
- // 清除异常
- env->ExceptionClear();
- }
JNI类型包括基本类型和引用(对象)类型。基本类型包括:jint、jbyte、jshort、jlong、jdouble、jboolean、jchar、jfloat等,如下图所示:
引用类型的负类是jobject,包含jclass、jstring、jarray,而jarray又包含各种基本类型对应的数组。层级关系如下图所示:
变量id用jfieldID表示,方法id用jmethodID表示。使用场景为反射Java变量或Java方法。比如,在反射Java方法时,先获取对应的jmethodID,再调用对应的method。
函数签名由参数类型和返回值组成,用参数个数、参数类型和返回值来区分同名方法,即解决方法重载问题。java基本类型对应的签名如下:
至于引用对象类型,使用类的全限定名作为签名。 比如String对应签名为Ljava/lang/String;
我们该如何反射调用java方法呢?首先要获取类的实例对象,然后获取方法id,最后根据方法id来调用方法。获取类的实例对象有两种方式:GetObjectClass()和FindClass(),示例如下:
- void get_class(JNIEnv *env, jobject object) {
- // 通过类的实例获取
- jclass clazz = env->GetObjectClass(object);
- // 通过类加载器查找指定的类
- jclass claxx = env->FindClass("java/lang/NullPointerException");
- }
我们可以通过GetObjectRefType()获取引用类型,包括如下引用类型:
- JNIInvalidRefType = 0
- JNILocalRefType = 1
- JNIGlobalRefType = 2
- JNIWeakGlobalRefType = 3
如果要判断是否属于某个类的实例,方法如下:
jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);
如果要判断两个对象是否相同,方法如下:
jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2);
反射Java的变量分为两步,首先获取变量的jfieldID,然后获取/设置变量值,示例如下:
- jclass clazz = env->GetObjectClass(object);
- jfieldID fieldId = env->GetFieldID(clazz, "level", "I");
- env->SetIntField(object, fieldId, 8);
反射Java的方法也分为两步,首先获取方法的jmethodID,然后调用方法,示例如下:
- jclass clazz = env->GetObjectClass(object);
- jmethodID methodId = env->GetMethodID(clazz, "setLevel", "(I)V");
- env->CallIntMethod(object, methodId, 8);
如果要读取来自Java层的字符串,可以调用GetStringUTFChars(),使用完毕不要忘记释放资源,否则导致内存泄漏。示例代码如下:
- void get_string_from_java(JNIEnv *env, jobject object, jstring jstr) {
- const char *str = env->GetStringUTFChars(jstr, JNI_FALSE);
- int len = env->GetStringUTFLength(jstr);
- printf("from java str=%s, len=%d", str, len);
- env->ReleaseStringUTFChars(jstr, str);
- }
如果要返回字符串给Java层,使用NewStringUTF(),示例代码如下:
- jstring set_string_to_java(JNIEnv *env, jobject object) {
- const char *str = "hello, world";
- return env->NewStringUTF(str);
- }
如果要读取来自Java层的数组,可以调用GetXxxArrayElements()。也可以调用GetXxxArrayRegion(),该函数比较灵活,支持指定数组区间。还有没有第三种方式呢?答案是有的,可以调用GetPrimitiveArrayCritical()获取原始数组,采用内存映射实现。示例代码如下:
- void get_array_from_java(JNIEnv *env, jobject object, jintArray jarray) {
- int len = env->GetArrayLength(jarray);
- // 1、使用GetIntArrayElements,使用完释放内存
- jint *array = env->GetIntArrayElements(jarray, JNI_FALSE);
- for (int i = 0; i < len; ++i) {
- printf("from java array=%d", array[i]);
- }
- env->ReleaseIntArrayElements(jarray, array, JNI_ABORT);
- // 2、使用GetIntArrayRegion,内部会释放内存
- env->GetIntArrayRegion(jarray, 0, len, array);
- // 3、使用GetPrimitiveArrayCritical获取原始数组
- array = (jint*) env->GetPrimitiveArrayCritical(jarray, JNI_FALSE);
- }
如果要返回数组给Java层,先创建JNI数组,然后把数据拷贝给数据,示例代码如下:
- jintArray set_array_to_java(JNIEnv *env, jobject object) {
- jint data[] = {1, 2, 3, 4, 5, 6};
- int size = sizeof(data)/sizeof(data[0]);
- jintArray array = env->NewIntArray(size);
- env->SetIntArrayRegion(array, 0, size, data);
- return array;
- }
我们可以在本地方法访问java.nio的DirectBuffer。先科普一下,DirectBuffer为堆外内存,实现零拷贝,提升Java层与native层的传输效率。而HeapBuffer为堆内存,在native层多一次拷贝,效率相对低。两者对比如下:
内存位置 | 使用场景 | 优点 | 缺点 | |
DirectBuffer | 堆外内存 | 调用频率高、数据多 | 零拷贝,效率高 | 创建耗时 |
HeapBuffer | 堆内存 | 调用频率低、数据少 | 创建相对快 | 存在拷贝,效率低 |
DirectBuffer在Native层的使用,可以在Java层创建,然后把对象传递到Native层。获取到内存地址后,把数据拷贝给DirectBuffer。整个过程如下:
- void copy_to_directBuffer(JNIEnv *env, jobject object, jobject buf) {
- uint8_t data[] = {1, 2, 3, 4, 5, 6};
- uint8_t *buf_addr = (uint8_t *) (env->GetDirectBufferAddress(buf));
- int buf_size = env->GetDirectBufferCapacity(buf);
- int data_size = sizeof(data)/sizeof(data[0]);
- int size = data_size > buf_size ? buf_size : data_size;
- memcpy(buf_addr, data, size);
- }
上面提及到反射调用Java方法,如果要根据method去获取对应id,API方法如下:
jmethodID FromReflectedMethod(JNIEnv *env, jobject method);
相反地,如果要根据id去获取对应method,API方法如下:
jobject ToReflectedMethod(JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic);
调用System.loadLibrary()时,系统在加载库成功后,会回调JNI_OnLoad(JavaVM *vm, void *reserved)。带有JavaVM参数可以保存为全局变量,返回值为JNI版本号。示例代码如下:
- jint JNI_OnLoad(JavaVM *vm, void *reserved) {
- javaVM = vm;
- return JNI_VERSION_1_6;
- }
当类加载器包含的本地库已经被垃圾回收器回收了,虚拟机会回调JNI_OnUnload()方法。 在该方法回调时,我们可以做内存清理工作。
2.1 创建JavaVM
创建JavaVM需要传入JavaVM指针、JNIEnv指针和VM参数。当前线程变成主线程,得到的env作为主线程的上下文环境。创建JVM方法为JNI_CreateJavaVM()。
2.2 关联JavaVM
当工作线程需要使用env时,必须先调用AttachCurrentThread()方法来关联JVM,因为env是线程私有的上下文环境。如果已经关联,不执行任何操作。需要注意的是,一个本地线程不能关联两个JVM。
2.3 脱离JavaVM
当使用完env时,调用DetachCurrentThread()方法来脱离JVM。
2.4 销毁JavaVM
当不再需要使用JavaVM时,调用DestroyJavaVM()方法用于卸载JVM和清除内存。任何线程,不管有没关联JVM,都可以调用该方法。
JavaVM的完整使用过程如下:
- void callJVM() {
- JNIEnv *env = nullptr;
- JavaVM *jvm = nullptr;
- // 1、创建jvm
- JNI_CreateJavaVM(&jvm, &env, nullptr);
- // 2、关联jvm
- jvm->AttachCurrentThread(&env, nullptr);
- // 3、do something with env
- // 4、脱离jvm
- jvm->DetachCurrentThread();
- // 5、销毁jvm
- jvm->DestroyJavaVM();
- }
做JNI/NDK开发时,经常遇到堆栈崩溃问题,只有一堆杂乱地址,实在让人摸不着头脑。堆栈信息包括:ABI架构、pid进程号、出错信号、崩溃原因、寄存器状态、堆栈地址。空指针引起的崩溃如下图所示:
字符串编码不同而引起的崩溃如下:
- Abort message: 'JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal continuation byte 0
- string: '�'
- input: '0xf4'
遇到native层崩溃时,我们可用ndk-stack查看堆栈地址,命令如下:
adb logcat | ndk-stack -sym xxx/libxxx.so
- // 0x12345678为堆栈地址,替换为实际崩溃地址
- aarch64-linux-android-addr2line -e libxxx.so 0x12345678
objdump可以用-syms查看符号表,命令如下:
objdump -syms libxxx.so
readelf是用来查看ELF文件的工具,ELF(Executable and Linkable Format)是一种可执行、可重定向的二进制目标文件。命令参数选项如下:
使用readelf -d libxxx.so查看其依赖库:
使用readelf -s libxxx.so查看其符号表:
参考链接:JNI官方开发指南
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。