当前位置:   article > 正文

Android的JNI开发全面介绍与最佳实践_android jni

android jni

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

目录

一、JNI整体设计

1、库的加载

2、动态注册与静态注册

3、JNI方法参数

4、全局引用与局部引用

5、异常检测与异常处理

二、JNI类型与数据结构

1、基本类型与引用类型

2、变量id与方法id

3、函数签名

三、JNI函数

1、获取类的实例对象

2、对象的操作

3、反射调用Java变量

4、反射调用Java方法

5、字符串的操作

6、数组的操作

7、NIO的创建与处理

8、方法与ID的转换

四、库加载回调与JavaVM调用

1、库加载回调

2、JavaVM调用

五、堆栈崩溃排查

1、ndk-stack查看堆栈

2、addr2line查看代码位置

3、objdump查看符号表

4、readelf查看依赖库与符号表


一、JNI整体设计

1、库的加载

在Android提供System.loadLibrary()或者System.load()来加载库。示例如下:

  1. static {
  2. try {
  3. System.loadLibrary("hello");
  4. } catch (UnsatisfiedLinkError error) {
  5. Log.e(TAG, "load library error=" + error.getMessage());
  6. }
  7. }

需要注意的是,如果.so动态库或.a静态库不存在时,会抛出couldn't find "libxxx.so"异常:

  1. load library error=dalvik.system.PathClassLoader[DexPathList[[
  2. zip file "/data/app/com.frank.ffmpeg/base.apk"],
  3. nativeLibraryDirectories=[/data/app/com.frank.ffmpeg/lib/arm64,
  4. data/app/com.frank.ffmpeg/base.apk!/lib/arm64-v8a,/system/lib64, /vendor/lib64, /product/lib64]]]
  5. 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(),源码如下:

  1. synchronized void loadLibrary0(ClassLoader loader, String libraryName) {
  2. if (loader != null) {
  3. // 1、调用classLoader查找库
  4. String filename = loader.findLibrary(libraryName);
  5. if (filename == null) {
  6. throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
  7. System.mapLibraryName(libraryName) + "\"");
  8. }
  9. // 2、调用native方法来加载
  10. String error = nativeLoad(filename, loader);
  11. if (error != null) {
  12. throw new UnsatisfiedLinkError(error);
  13. }
  14. return;
  15. }
  16. // 3、拼接完整库名,比如由hello拼接成libhello.so
  17. String filename = System.mapLibraryName(libraryName);
  18. List<String> candidates = new ArrayList<String>();
  19. String lastError = null;
  20. for (String directory : getLibPaths()) {
  21. String candidate = directory + filename;
  22. candidates.add(candidate);
  23. if (IoUtils.canOpenReadOnly(candidate)) {
  24. // 4、调用native方法来加载
  25. String error = nativeLoad(candidate, loader);
  26. if (error == null) {
  27. return; // 加载library成功
  28. }
  29. lastError = error;
  30. }
  31. }
  32. if (lastError != null) {
  33. throw new UnsatisfiedLinkError(lastError);
  34. }
  35. throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
  36. }

这里的nativeLoad()属于runtime底层的jni方法,接着调用art/runtime/java_vm_ext.cc的load_NativeLibrary(),最终调用dlopen()来打开so库或a库。 调用过程如下图:

2、动态注册与静态注册

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"关键字):

  1. #ifdef __cplusplus
  2. extern "C" {
  3. #endif
  4. JNIEXPORT void JNICALL Java_com_frank_ffmpeg_FFmpegCmd_hello(JNIEnv *env, jclass thiz, jint num) {
  5. }
  6. #ifdef __cplusplus
  7. }
  8. #endif

如果觉得每个JNI方法都这样写比较麻烦,我们可以写个宏定义:

  1. #define FFMPEG_FUNC(RETURN_TYPE, FUNC_NAME, ...) \
  2. JNIEXPORT RETURN_TYPE JNICALL Java_com_frank_ffmpeg_FFmpegCmd_ ## FUNC_NAME \
  3. (JNIEnv *env, jclass thiz, ##__VA_ARGS__)\

动态注册的示例:

  1. JNINativeMethod nativeMethods[] {
  2. {"hello", "(I)V", (void *)"native_hello"},
  3. {"world", "(J)V", (void *)"native_world"}
  4. };
  5. jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  6. JNIEnv *env = NULL;
  7. vm->GetEnv((void **)&env, JNI_VERSION_1_6);
  8. jclass clazz = env->FindClass("com/frank/ffmpeg/handler/FFmpegHandler");
  9. int numMethods = sizeof(nativeMethods) / sizeof(nativeMethods[0]);
  10. // 注册本地方法到函数表
  11. env->RegisterNatives(clazz, nativeMethods, numMethods);
  12. env->DeleteLocalRef(clazz);
  13. return JNI_VERSION_1_6;
  14. }

JNINativeMethod的结构体位于jni.h,定义如下:

  1. typedef struct {
  2. const char* name;
  3. const char* signature;
  4. void* fnPtr;
  5. } JNINativeMethod;

3、JNI方法参数

JNI方法前两个参数分别是JNIEnv和jclass,其中JNIEnv是上下文环境,而jclass是类的实例对象。其他参数为带j开头,比如jint、jstring。

4、全局引用与局部引用

JNI提供局部引用和全局引用,还有全局弱引用。顾名思义,局部引用的作用域为局部,在本地方法返回时被GC主动回收,通过如下方法创建:

jobject NewLocalRef(JNIEnv *env, jobject ref);

全局引用的作用域为全局,不会被GC回收,需要手动释放引用资源,否则导致内存泄漏。全局引用的创建与释放如下:

  1. // new global reference
  2. jobject NewGlobalRef(JNIEnv *env, jobject obj);
  3. // delete global reference
  4. void DeleteGlobalRef(JNIEnv *env, jobject globalRef);

全局弱引用与全局引用不同的是,它可以被GC回收。另外,它关联到虚引用,用于感知何时被GC回收。全局弱引用的创建与释放如下:

  1. // new weak global reference
  2. jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
  3. // delete weak global reference
  4. void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);

5、异常检测与异常处理

JNI提供检测异常、抛出异常和清除异常。使用ExceptionOccurred()进行异常检测。在检测到异常后通过ThrowNew()抛出异常,方法如下:

jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);

最后是清除异常,使用ExceptionClear()。完整的示例代码如下:

  1. // 检测异常
  2. if (env->ExceptionOccurred() != NULL) {
  3. // 抛出异常
  4. jclass clazz = env->FindClass("java/lang/NullPointerException");
  5. env->ThrowNew(clazz, "This is a null pointer...");
  6. // 清除异常
  7. env->ExceptionClear();
  8. }

二、JNI类型与数据结构

1、基本类型与引用类型

JNI类型包括基本类型和引用(对象)类型。基本类型包括:jint、jbyte、jshort、jlong、jdouble、jboolean、jchar、jfloat等,如下图所示:

引用类型的负类是jobject,包含jclass、jstring、jarray,而jarray又包含各种基本类型对应的数组。层级关系如下图所示:

2、变量id与方法id

变量id用jfieldID表示,方法id用jmethodID表示。使用场景为反射Java变量或Java方法。比如,在反射Java方法时,先获取对应的jmethodID,再调用对应的method。

3、函数签名

函数签名由参数类型和返回值组成,用参数个数、参数类型和返回值来区分同名方法,即解决方法重载问题。java基本类型对应的签名如下:

至于引用对象类型,使用类的全限定名作为签名。 比如String对应签名为Ljava/lang/String;

三、JNI函数

1、获取类的实例对象

我们该如何反射调用java方法呢?首先要获取类的实例对象,然后获取方法id,最后根据方法id来调用方法。获取类的实例对象有两种方式:GetObjectClass()和FindClass(),示例如下:

  1. void get_class(JNIEnv *env, jobject object) {
  2. // 通过类的实例获取
  3. jclass clazz = env->GetObjectClass(object);
  4. // 通过类加载器查找指定的类
  5. jclass claxx = env->FindClass("java/lang/NullPointerException");
  6. }

2、对象的操作

我们可以通过GetObjectRefType()获取引用类型,包括如下引用类型:

  1. JNIInvalidRefType = 0
  2. JNILocalRefType = 1
  3. JNIGlobalRefType = 2
  4. JNIWeakGlobalRefType = 3

 如果要判断是否属于某个类的实例,方法如下:

jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);

如果要判断两个对象是否相同,方法如下:

jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2);

3、反射调用Java变量

反射Java的变量分为两步,首先获取变量的jfieldID,然后获取/设置变量值,示例如下:

  1. jclass clazz = env->GetObjectClass(object);
  2. jfieldID fieldId = env->GetFieldID(clazz, "level", "I");
  3. env->SetIntField(object, fieldId, 8);

4、反射调用Java方法

反射Java的方法也分为两步,首先获取方法的jmethodID,然后调用方法,示例如下:

  1. jclass clazz = env->GetObjectClass(object);
  2. jmethodID methodId = env->GetMethodID(clazz, "setLevel", "(I)V");
  3. env->CallIntMethod(object, methodId, 8);

5、字符串的操作

如果要读取来自Java层的字符串,可以调用GetStringUTFChars(),使用完毕不要忘记释放资源,否则导致内存泄漏。示例代码如下:

  1. void get_string_from_java(JNIEnv *env, jobject object, jstring jstr) {
  2. const char *str = env->GetStringUTFChars(jstr, JNI_FALSE);
  3. int len = env->GetStringUTFLength(jstr);
  4. printf("from java str=%s, len=%d", str, len);
  5. env->ReleaseStringUTFChars(jstr, str);
  6. }

如果要返回字符串给Java层,使用NewStringUTF(),示例代码如下:

  1. jstring set_string_to_java(JNIEnv *env, jobject object) {
  2. const char *str = "hello, world";
  3. return env->NewStringUTF(str);
  4. }

6、数组的操作

如果要读取来自Java层的数组,可以调用GetXxxArrayElements()。也可以调用GetXxxArrayRegion(),该函数比较灵活,支持指定数组区间。还有没有第三种方式呢?答案是有的,可以调用GetPrimitiveArrayCritical()获取原始数组,采用内存映射实现。示例代码如下:

  1. void get_array_from_java(JNIEnv *env, jobject object, jintArray jarray) {
  2. int len = env->GetArrayLength(jarray);
  3. // 1、使用GetIntArrayElements,使用完释放内存
  4. jint *array = env->GetIntArrayElements(jarray, JNI_FALSE);
  5. for (int i = 0; i < len; ++i) {
  6. printf("from java array=%d", array[i]);
  7. }
  8. env->ReleaseIntArrayElements(jarray, array, JNI_ABORT);
  9. // 2、使用GetIntArrayRegion,内部会释放内存
  10. env->GetIntArrayRegion(jarray, 0, len, array);
  11. // 3、使用GetPrimitiveArrayCritical获取原始数组
  12. array = (jint*) env->GetPrimitiveArrayCritical(jarray, JNI_FALSE);
  13. }

如果要返回数组给Java层,先创建JNI数组,然后把数据拷贝给数据,示例代码如下:

  1. jintArray set_array_to_java(JNIEnv *env, jobject object) {
  2. jint data[] = {1, 2, 3, 4, 5, 6};
  3. int size = sizeof(data)/sizeof(data[0]);
  4. jintArray array = env->NewIntArray(size);
  5. env->SetIntArrayRegion(array, 0, size, data);
  6. return array;
  7. }

7、NIO的创建与处理

我们可以在本地方法访问java.nio的DirectBuffer。先科普一下,DirectBuffer为堆外内存,实现零拷贝,提升Java层与native层的传输效率。而HeapBuffer为堆内存,在native层多一次拷贝,效率相对低。两者对比如下:

内存位置使用场景优点缺点
DirectBuffer堆外内存调用频率高、数据多零拷贝,效率高创建耗时
HeapBuffer堆内存调用频率低、数据少创建相对快存在拷贝,效率低

 DirectBuffer在Native层的使用,可以在Java层创建,然后把对象传递到Native层。获取到内存地址后,把数据拷贝给DirectBuffer。整个过程如下:

  1. void copy_to_directBuffer(JNIEnv *env, jobject object, jobject buf) {
  2. uint8_t data[] = {1, 2, 3, 4, 5, 6};
  3. uint8_t *buf_addr = (uint8_t *) (env->GetDirectBufferAddress(buf));
  4. int buf_size = env->GetDirectBufferCapacity(buf);
  5. int data_size = sizeof(data)/sizeof(data[0]);
  6. int size = data_size > buf_size ? buf_size : data_size;
  7. memcpy(buf_addr, data, size);
  8. }

8、方法与ID的转换

上面提及到反射调用Java方法,如果要根据method去获取对应id,API方法如下:

jmethodID FromReflectedMethod(JNIEnv *env, jobject method);

相反地,如果要根据id去获取对应method,API方法如下:

jobject ToReflectedMethod(JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic);

四、库加载回调与JavaVM调用

1、库加载回调

调用System.loadLibrary()时,系统在加载库成功后,会回调JNI_OnLoad(JavaVM *vm, void *reserved)。带有JavaVM参数可以保存为全局变量,返回值为JNI版本号。示例代码如下:

  1. jint JNI_OnLoad(JavaVM *vm, void *reserved) {
  2. javaVM = vm;
  3. return JNI_VERSION_1_6;
  4. }

当类加载器包含的本地库已经被垃圾回收器回收了,虚拟机会回调JNI_OnUnload()方法。 在该方法回调时,我们可以做内存清理工作。

2、JavaVM调用

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的完整使用过程如下:

  1. void callJVM() {
  2. JNIEnv *env = nullptr;
  3. JavaVM *jvm = nullptr;
  4. // 1、创建jvm
  5. JNI_CreateJavaVM(&jvm, &env, nullptr);
  6. // 2、关联jvm
  7. jvm->AttachCurrentThread(&env, nullptr);
  8. // 3、do something with env
  9. // 4、脱离jvm
  10. jvm->DetachCurrentThread();
  11. // 5、销毁jvm
  12. jvm->DestroyJavaVM();
  13. }

五、堆栈崩溃排查

做JNI/NDK开发时,经常遇到堆栈崩溃问题,只有一堆杂乱地址,实在让人摸不着头脑。堆栈信息包括:ABI架构、pid进程号、出错信号、崩溃原因、寄存器状态、堆栈地址。空指针引起的崩溃如下图所示:

字符串编码不同而引起的崩溃如下:

  1. Abort message: 'JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal continuation byte 0
  2. string: '�'
  3. input: '0xf4'

1、ndk-stack查看堆栈

遇到native层崩溃时,我们可用ndk-stack查看堆栈地址,命令如下:

adb logcat | ndk-stack -sym xxx/libxxx.so

2、addr2line查看代码位置

  1. // 0x12345678为堆栈地址,替换为实际崩溃地址
  2. aarch64-linux-android-addr2line -e libxxx.so 0x12345678

3、objdump查看符号表

objdump可以用-syms查看符号表,命令如下:

objdump -syms libxxx.so

4、readelf查看依赖库与符号表

readelf是用来查看ELF文件的工具,ELF(Executable and Linkable Format)是一种可执行、可重定向的二进制目标文件。命令参数选项如下:

使用readelf -d libxxx.so查看其依赖库:

使用readelf -s libxxx.so查看其符号表:

  

参考链接:JNI官方开发指南

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/575201
推荐阅读
相关标签
  

闽ICP备14008679号