Android JNI

JNI 是指 Java 原生接口。它定义了 Android 从受管理代码(使用 Java 或 Kotlin 编程语言编写)编译的字节码与原生代码(使用 C/C++ 编写)进行交互的方式。JNI 不依赖于供应商,支持从动态共享库加载代码,虽然有时较为繁琐,但效率尚可。

注意:由于 Android 采用与 Java 编程语言类似的方式将 Kotlin 编译为适合 ART 的字节码,因此您可以根据 JNI 架构及其相关成本将本页中的指南应用于 Kotlin 和 Java 编程语言。 如需了解详情,请参阅 Kotlin 和 Android

如果您还不熟悉 JNI,请仔细阅读 Java 原生接口规范,了解 JNI 的工作原理以及可用的功能。首次阅读时,接口的某些方面不会立即显而易见,因此接下来的几个部分可能会对您有所帮助。

如需浏览全局 JNI 引用并查看创建和删除全局 JNI 引用的位置,请使用 Android Studio 3.2 及更高版本的内存分析器中的 JNI 堆视图。

一般提示

尽量减少 JNI 层的占用空间。此时,需要考虑几个维度。 您的 JNI 解决方案应尝试遵循以下准则(按重要性顺序列出,从最重要的开始):

  • 尽可能减少跨 JNI 层编组资源的次数。跨 JNI 层编组的费用很高。尝试设计一个接口,以尽可能减少需要编组的数据量以及必须进行数据编组的频率。
  • 尽可能避免在使用受管理编程语言编写的代码与使用 C++ 编写的代码之间进行异步通信。 这样可使 JNI 接口更易于维护。通常,您可以采用与界面相同的语言来简化异步界面更新。例如,最好使用 Java 编程语言在两个线程之间执行回调,其中一个线程发出阻塞 C++ 调用,然后在阻塞调用完成后通知界面线程,而不是通过 JNI 从 Java 代码中的界面线程调用 C++ 函数。
  • 最大限度地减少需要接触 JNI 或被 JNI 接触的线程数。 如果您确实需要使用 Java 和 C++ 语言的线程池,请尝试在池所有者之间(而不是各个工作器线程之间)保持 JNI 通信。
  • 将接口代码保存在少量易于识别的 C++ 和 Java 源代码位置,便于未来的重构工作。请考虑视情况使用 JNI 自动生成库。

JavaVM 和 JNIEnv

JNI 定义了两个关键数据结构,即“JavaVM”和“JNIEnv”。两者本质上都是指向函数表的指针。(在 C++ 版本中,它们是一些类,这些类具有指向函数表的指针,以及通过表间接传递的每个 JNI 函数的成员函数)。JavaVM 提供“调用接口”函数,用于创建和销毁 JavaVM。理论上,每个进程可以有多个 JavaVM,但 Android 只允许有一个。

JNIEnv 提供了大部分 JNI 函数。您的原生函数都会接收 JNIEnv 作为第一个参数,但 @CriticalNative 方法除外,请参阅更快的原生调用

该 JNIEnv 将用于线程本地存储。因此,您无法在线程之间共享 JNIEnv。如果代码段无法通过其他方法获取其 JNIEnv,您应该共享 JavaVM,并使用 GetEnv 发现线程的 JNIEnv。(假设该线程包含一个 JNIEnv;请参阅下面的 AttachCurrentThread。)

JNIEnv 和 JavaVM 的 C 声明与 C++ 声明不同。"jni.h" 包含文件会提供不同的类型定义符,具体取决于该文件是包含在 C 还是 C++ 中。因此,我们不建议在这两种语言包含的头文件中包含 JNIEnv 参数。(换个说法:如果您的头文件需要 #ifdef __cplusplus,且该头文件中的任何内容引用 JNIEnv,您可能需要执行一些额外的操作。)

Threads

所有线程都是 Linux 线程,由内核调度。它们通常从受管理代码启动(使用 Thread.start()),但也可以在其他位置创建,然后附加到 JavaVM。例如,可以使用 AttachCurrentThread() 或 AttachCurrentThreadAsDaemon() 函数附加使用 pthread_create() 或 std::thread 启动的线程。在附加之前,线程没有 JNIEnv,也无法进行 JNI 调用

通常,最好使用 Thread.start() 创建任何需要调用 Java 代码的线程。这样做可确保您拥有足够的堆栈空间、位于正确的 ThreadGroup 中,且与 Java 代码使用相同的 ClassLoader。此外,设置线程名称以在 Java 中进行调试也比从原生代码中更容易(如果您有 pthread_t 或 thread_t,请参阅 pthread_setname_np();如果您有 std::thread 并且需要 pthread_t,请参阅 std::thread::native_handle())。

附加原生创建的线程会构建 java.lang.Thread 对象并将其添加到“主”ThreadGroup,使其对调试程序可见。在已附加的线程上调用 AttachCurrentThread() 属于空操作。

Android 不会挂起执行原生代码的线程。如果正在进行垃圾回收,或调试程序已发出挂起请求,Android 将在下次调用 JNI 时暂停线程。

通过 JNI 附加的线程必须在退出之前调用 DetachCurrentThread()。如果直接对此进行编码会很棘手,在 Android 2.0 (Eclair) 及更高版本中,您可以使用 pthread_key_create() 定义要在线程退出之前调用的析构函数,然后从该函数调用 DetachCurrentThread()。(将该键与 pthread_setspecific() 搭配使用,以将 JNIEnv 存储在 thread-local-storage 中;这样一来,该键将作为参数传入您的析构函数。)

jclass、jmethodID 和 jfieldID

如果要通过原生代码访问对象的字段,请执行以下操作:

  • 使用 FindClass 获取类的类对象引用
  • 使用 GetFieldID 获取字段的字段 ID
  • 使用适当内容获取字段的内容,例如 GetIntField

同样,如需调用方法,首先要获取类对象引用,然后获取方法 ID。ID 通常只是指向内部运行时数据结构的指针。查找这些信息可能需要进行多次字符串比较,但获得这些信息后,您可以非常快速地进行实际调用以获取字段或调用相应方法。

如果性能很重要,最好查找一次值并将结果缓存在原生代码中。由于每个进程仅限一个 JavaVM,因此可以将此数据存储在静态本地结构中。

在取消加载类之前,类引用、字段 ID 和方法 ID 保证有效。只有当与 ClassLoader 关联的所有类都可以进行垃圾回收时,系统才会卸载类,这种情况很少见,但在 Android 中并非不可能。但请注意,jclass 是类引用,必须通过调用 NewGlobalRef 来保护(请参阅下一部分)。

如果您想在加载类时缓存 ID,并在类被取消加载并重新加载时自动重新缓存,初始化 ID 的正确方法是将类似如下的一段代码添加到相应类:

KotlinJava

companion object {
    /*
     * We use a static class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private external fun nativeInit()

    init {
        nativeInit()
    }
}

在执行 ID 查找的 C/C++ 代码中创建 nativeClassInit 方法。该代码将在初始化类时执行一次。如果取消加载该类后又重新加载,它将再次执行。

局部引用和全局引用

传递给原生方法的每个参数,以及 JNI 函数返回的几乎每个对象都是“局部引用”。这意味着,它在当前线程中的当前原生方法运行期间有效。 在原生方法返回后,即使对象本身继续存在,该引用也无效。

这适用于 jobject 的所有子类,包括 jclassjstring 和 jarray。(启用扩展 JNI 检查后,运行时会针对大多数引

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值