关于android:JNI之常见技巧与陷阱

4次阅读

共计 4423 个字符,预计需要花费 12 分钟才能阅读完成。

预报

后续可能会推更一个 FFmpeg 系列的入门博客,大略波及到 FFmpeg 解封装、FFmpeg 编解码、FFmpeg 进行音频重采样、应用 FFMpeg 将 mp3 转码成 aac、应用 FFmpeg 合并拼接音视频等。

另外如果有工夫可能也会更新几篇对于 ffplay 的文章,敬请关注。

本文将作为 JNI 系列的一个结尾,上面是笔者在学习应用 JNI 的所记录的一些笔记与技巧。

JNIEnv 的线程限度

一个 JNIEnv 指针仅在其相关联的线程中无效。你不能将这个指针从一个线程中传递给另一个线程,或者在多线程中缓存和应用它。Java 虚拟机在同一个线程的间断调用中传递给本地办法雷同的 JNIEnv 指针,然而从不同线程中调用本地办法时传递的是不同的 JNIEnv 指针。该当防止缓存一个线程的 JNIEnv 指针并在另一个线程中应用指针的常见谬误。

本地援用 (部分援用) 仅在创立它的线程中无效。你不能将本地援用从一个线程中传递到另一个线程。每当有多个线程可能应用雷同援用的可能性时,应始终将本地援用转换为全局援用。

JNIEnv 是用作线程部分存储。因而,使用者不能在多线程间共享一个 JNIEnv 变量。如果在一段代码中没有其它方法取得它的 JNIEnv,使用者能够共享 JavaVM 对象,应用 GetEnv 来获得该线程下的 JNIEnv。

如果你应用 AttachCurrentThread 连贯(attach)了 Native 过程,正在运行的代码在线程拆散(detach)之前绝不会主动开释部分援用。使用者创立的任何部分援用必须手动删除。通常,任何在循环中创立部分援用的 Native 代码可能都须要做一些手动删除。

全局获取 JNIEnv

一个 JNIEnv 指针仅在与其相关联的线程中无效。对于本地办法,这通常不是问题,因为他们从虚拟机承受 JNIEnv 指针作为第一个参数。然而有时候可能不须要间接从虚拟机调用的本地代码来获取属于以后线程的 JNIEnv 接口指针。例如通过 JNI 在 Native 开启了一个子线程解决某些工作,在这些工作处理完毕后须要将处理结果回调给 java 层。
这种状况能够通过缓存 JavaVM 获取以后线程的 JNIEnv 而后进行 java 办法的回调。

当 System 加载一个本地库时,虚构机会在本地库中查找下述的导出的程序入口:

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved);

此时咱们能够将 JavaVM 缓存下来,供当前获取 JNIEnv 应用。

上面是在任何地位获取 JNIEnv 的例子:

JavaVM *globalJVM = nullptr;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
    globalJVM = jvm;
    return JNI_VERSION_1_6;
}

JNIEnv *getCurrentEnv(int *attach) {if (globalJVM == nullptr) return nullptr;
    *attach = 0;
    JNIEnv *jni_env = nullptr;
    int result = globalJVM->GetEnv((void **) &jni_env, JNI_VERSION_1_6);
    if (result == JNI_EDETACHED || jni_env == nullptr) {result = globalJVM->AttachCurrentThread(&jni_env, nullptr);
        if (result < 0) {jni_env = nullptr;} else {*attach = 1;}
    }
    return jni_env;
}

不要混同 ID 和援用

JNI 将对象作为援用。类,字符串和数组是非凡类型的援用。JNI 将办法和字段作为 ID。一个 ID 不是一个参考。不要将类援用称为“类 ID”,也不要将办法 ID 称为“办法援用”。

援用是能够由本地代码显式治理的虚拟机资源。例如,JNI 函数 DeleteLocalRef 容许本地代码删除本地援用。相比之下,字段和办法 ID 由虚拟机治理并放弃无效直到其定义的类被卸载。在虚拟机卸载定义的类之前,本机代码不能显式删除字段或办法 ID。

缓存字段和办法 ID

本地代码通过将字段或办法的名称和类型描述符指定为字符串而后从虚拟机获取字段或办法 ID。应用名称和类型字符串的字段和办法查找速度很慢。缓存这些 ID 通常是无利的,未能缓存字段和办法 ID 是本机代码中的常见性能问题。

缓存字段或办法 ID 倡议应用类的动态代码块的形式进行缓存。

防止适量创立本地援用

尽管说本地援用会在函数完结时主动开释,然而 JNI 对于本地援用的个数是有肯定的限度的,个别是限度到 512 个,因而须要留神一些调用链比拟长的函数或者是在循环体内返回的本地援用在应用结束后及时进行开释,以保障 GC 的失常工作和内存的稳固。

NDK 谬误定位

在开发的过程中常常会呈现一些 Native 层的解体,而后在 Logcat 中又没有显示具体位置的,这时候能够应用 NDK 中的 addr2line 工具包进行定位。

addr2line 的命令应用形式如下:

addr2line 的绝对路径 -C -f -e so 文件的绝对路径  谬误内存地址

其中 -C - f 示意打印谬误行数所在的函数名称,- e 示意打印谬误地址的对应门路及行数。
留神不同的 CPU 架构须要应用不同的 addr2line,比方 mac 零碎的 addr2line 就存在于ndk 目录 /toolchains/llvm/prebuilt/darwin-x86_64/bin

那么怎么通过 Logcat 定位到解体的内存地址呢?例如有以下解体日志:

2022-03-29 22:28:58.462 22761-22761/? A/DEBUG: ABI: 'arm64'
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG: Timestamp: 2022-03-29 22:28:58+0800
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG: pid: 22652, tid: 22733, name: Thread-2  >>> com.fly.jnitest <<<
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG: uid: 10147
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG: signal 5 (SIGTRAP), code 1 (TRAP_BRKPT), fault addr 0x7984411f40
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG:     x0  00000079de54d200  x1  0000007a73fd41c0  x2  0000000000000000  x3  00000079eec9fcda
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG:     x4  00000079837e5c08  x5  00000079eead6059  x6  0000000000000001  x7  00000079837e5838
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG:     x8  0000000000000000  x9  b4175a2f4989bf75  x10 0000000000430000  x11 0000000000000001
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG:     x12 0000000000000000  x13 0000000000000000  x14 0000000000000012  x15 00000000000000ff
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG:     x16 00000079ef1c8748  x17 0000007a73c62350  x18 00000079802a6000  x19 00000079837e5d50
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG:     x20 0000007a700ee0dc  x21 00000079837e5d50  x22 0000587c0000587c  x23 00000079837e5dd8
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG:     x24 00000079837e5d50  x25 00000079837e5d50  x26 00000079837e6020  x27 0000007a74148020
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG:     x28 0000007ffad6a430  x29 00000079837e5cf0
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG:     sp  00000079837e5ce0  lr  0000007984411f3c  pc  0000007984411f40
2022-03-29 22:28:58.462 22761-22761/? A/DEBUG: backtrace:
2022-03-29 22:28:58.463 22761-22761/? A/DEBUG:       #00 pc 0000000000000f40  /data/app/com.fly.jnitest-X01T6VOuYKufX3tBWVg2vA==/lib/arm64/libjnitest.so (test(void*)+24) (BuildId: f06b5f684113a965be07abbcf0bb4e5488d31870)
2022-03-29 22:28:58.463 22761-22761/? A/DEBUG:       #01 pc 00000000000e1100  /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+36) (BuildId: c042ffb4e195c9462700c20f99189c2b)
2022-03-29 22:28:58.463 22761-22761/? A/DEBUG:       #02 pc 0000000000083ab0  /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: c042ffb4e195c9462700c20f99189c2b)

那么 backtrace: 所在的下一行就是解体的内存地址,也就是说下面解体日志的谬误地址是0000000000000f40

参考资料

《JNI 编程指南与标准》

举荐浏览

JNI 根底简介
JNI 之数组与字符串的应用
JNI 之动静注册与动态注册
JNI 之拜访 java 属性和办法
JNI 之缓存与援用
JNI 之异样解决

关注我,一起提高,人生不止 coding!!!

正文完
 0