JNI之常见技巧与陷阱

简介: NDK/JNI连载系列

预告

后续可能会推更一个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!!!

目录
相关文章
|
Cloud Native Java 编译器
Java生态系统的进化:从JDK 1.0到今天
Java生态系统的进化:从JDK 1.0到今天
|
4月前
|
机器学习/深度学习 人工智能 前端开发
基于YOLOv8的桥梁八类缺陷、病害高精度检测项目|完整源码数据集+PyQt5界面+完整训练流程+开箱即用!
本项目基于YOLOv8与PyQt5开发,实现桥梁八类病害(如裂缝、腐蚀、混凝土退化等)的高精度自动检测,支持图片、视频、摄像头等多种输入方式,附带完整源码、数据集与训练教程,开箱即用,适用于科研、工程与教学场景。
基于YOLOv8的桥梁八类缺陷、病害高精度检测项目|完整源码数据集+PyQt5界面+完整训练流程+开箱即用!
|
4月前
|
监控 Linux 网络安全
Linux命令大全:从入门到精通
日常使用的linux命令整理
809 13
|
9月前
|
SQL JSON 数据可视化
基于 DIFY 的自动化数据分析实战
本文介绍如何使用DIFY搭建数据分析自动化流程,实现从输入需求到查询数据库、LLM分析再到可视化输出的全流程。基于经典的employees数据集和DIFY云端环境,通过LLM-SQL解析、SQL执行、LLM数据分析及ECharts可视化等模块,高效完成数据分析任务。此方案适用于人力资源分析、薪酬管理等数据密集型业务,显著提升效率并降低成本。
13465 16
|
存储 SQL 关系型数据库
MySQL语句详解:从基础到进阶的全面指南
MySQL语句详解:从基础到进阶的全面指南
|
11月前
|
人工智能 IDE 程序员
GitHub Copilot 免费了!程序员们的福音来了!
《GitHub Copilot 免费了!程序员们的福音来了!》 近日,GitHub 宣布其 AI 编程助手 GitHub Copilot 现在可以免费使用。曾经每月需支付 10 美元订阅费的 Copilot,现在向所有人开放免费版本,这对个人开发者、初学者和小型团队来说是个大好消息。免费版支持 GPT 和 Claude 模型,并提供每月 2000 次代码补全和 50 条聊天消息等核心功能。用户只需注册或登录 GitHub 账户,在 VS Code 中安装扩展并激活免费版即可使用。此外,Visual Studio Code 也完全免费,进一步降低了开发门槛。 除了
11993 7
GitHub Copilot 免费了!程序员们的福音来了!
|
12月前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
495 5
|
Java 开发工具 数据库
入职必会-开发环境搭建01-JDK下载和安装
JDK(Java Development Kit)是Java开发工具包,包含Java虚拟机(JVM)、Java类库及开发工具,如调试器、性能分析工具和文档生成工具。JVM执行Java字节码,类库提供预定义类和方法简化开发,开发工具助力高效开发、调试和优化Java应用。
331 5
入职必会-开发环境搭建01-JDK下载和安装
|
机器学习/深度学习 人工智能 算法
【机器学习】机器学习与AI大数据的融合:开启智能新时代
【机器学习】机器学习与AI大数据的融合:开启智能新时代
661 1