JNI 提示

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

注意:由于 Android 会将 Kotlin 编译为 与 Java 编程语言类似,您可以将此页面上的指南应用于 Kotlin 和 Java 编程语言。 如需了解详情,请参阅 Kotlin 和 Android

如果您对此还不熟悉,请仔细阅读 Java 原生接口规范 了解 JNI 的工作原理和可用功能。部分 界面的各个方面 因此,后面的几节内容对您来说可能比较实用。

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

常规提示

尽量减少 JNI 层的占用空间。这种情况下,需要考虑多个维度。 您的 JNI 解决方案应尽量遵循以下准则(按重要性顺序列出, 先从最重要的部分开始):

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

JavaVM 和 JNIEnv

JNI 定义了两个关键数据结构,即“JavaVM”和“JNIEnv”。从本质上讲, 指向函数表的指针的指针。(在 C++ 版本中,它们是 指向函数表的指针,以及每个通过 表格。)JavaVM 提供“调用接口”函数, 支持创建和销毁 JavaVM从理论上讲,每个进程可以有多个 JavaVM, 但 Android 仅允许一个。

JNIEnv 提供了大部分 JNI 函数。您的原生函数都会以如下形式接收 JNIEnv: 第一个参数(@CriticalNative 方法除外) 请参阅加快原生调用速度

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

JNIEnv 和 JavaVM 的 C 声明不同于 C++ 声明。"jni.h" include 文件提供不同的类型定义符 具体取决于是包含在 C 还是 C++ 中。因此,我们不建议 在这两种语言包含的头文件中添加 JNIEnv 参数。(换个说法:如果您的 头文件需要 #ifdef __cplusplus,但如果其中含有 该标头引用 JNIEnv。)

Threads

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

通常,最好使用 Thread.start() 来创建需要 调用 Java 代码这样做可以确保您有足够的堆栈空间 位于正确的 ThreadGroup 中,并且您使用的是相同的 ClassLoader 编码为 Java 代码在 Java 中设置线程名称以进行调试也比从 (如果您有 pthread_tpthread_setname_np() thread_tstd::thread::native_handle()(如果您有 std::thread 且想要 pthread_t)。

附加原生创建的线程会导致 java.lang.Thread 要构建的对象并将其添加到“main”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,并自动重新缓存它们 如果类被卸载并重新加载,初始化的正确方式 将类似于以下内容的一段代码添加到相应的类中:

Kotlin

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()
    }
}

Java

    /*
     * We use a 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 static native void nativeInit();

    static {
        nativeInit();
    }

在执行 ID 查找的 C/C++ 代码中创建 nativeClassInit 方法。代码 将在类初始化时执行一次。如果该类被卸载,并且 然后重新加载,系统会再次执行该函数。

局部引用和全局引用

传递给原生方法的每个参数,以及返回的每个对象 属于“局部引用”也就是说,该日期在 当前原生方法在当前线程中的持续时间。 即使对象本身在原生方法之后继续存在, 则引用无效。

这适用于 jobject 的所有子类,包括 jclassjstringjarray。 (扩展 JNI 时,运行时会针对大部分引用误用问题向您发出警告 检查是否处于启用状态)。

获取非局部引用的唯一方法是通过函数 NewGlobalRefNewWeakGlobalRef

如果您想将某个参考文件保留更长时间,就必须使用 “全局”参考。NewGlobalRef 函数接受 本地引用作为参数,并返回全局引用。 在调用 DeleteGlobalRef

此模式通常在缓存返回的 jclass 时使用 来自 FindClass,例如:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

所有 JNI 方法都接受局部引用和全局引用作为参数。 对同一对象的引用可能具有不同的值。 例如,对 同一对象的 NewGlobalRef 可能有所不同。 如需查看两个引用是否引用了同一个对象, 必须使用 IsSameObject 函数。一律不比较 在原生代码中使用 == 引用引用。

这样做的一个后果就是 不得假设对象引用是常量或唯一的 。表示对象的值可能不同 在该方法的一次调用之间执行, 不同的对象在连续调用中可能具有相同的值。请勿使用 jobject 值作为键。

程序员需要“不过度分配”局部引用。实际上,这意味着 如果您要创建大量局部引用,也许是在运行 您应使用以下代码手动释放它们: DeleteLocalRef,而不是让 JNI 为您执行此操作。通过 只需为容器预留槽位 16 个局部引用,因此如果您需要更多引用,则应边操作边删除,或使用 EnsureLocalCapacity/PushLocalFrame(预订更多)。

请注意,jfieldIDjmethodID 是不透明的 类型,而不是对象引用,并且不应将其传递给 NewGlobalRef。原始数据 GetStringUTFChars 等函数返回的指针 和 GetByteArrayElements 也不是对象。(它们可能被 在线程间使用,并且在匹配的 Release 调用之前有效。)

还有一种不寻常的情况值得单独提及。如果您将原生 AttachCurrentThread 之间的线程,您运行的代码将 在线程分离之前,绝不会自动释放局部引用。任何本地 您必须手动删除这些参考文件。一般来说, 在循环中创建局部引用的代码可能需要手动执行一些操作, 删除。

请谨慎使用全局引用。全局引用不可避免,但很困难 并且可能导致难以诊断的内存(不良)行为。在所有其他条件均相同的情况下 全局引用越少,解决方案就越好。

UTF-8 和 UTF-16 字符串

Java 编程语言使用的是 UTF-16。为方便起见,JNI 提供了 还经过修改的 UTF-8。通过 经过修改的编码对于 C 代码非常有用,因为它将 \u0000 编码为 0xc0 0x80,而不是 0x00。 这有个好处,就是你可以使用以零终止的 C 样式的字符串, 且适合与标准 libc 字符串函数搭配使用。但其缺点是,您无法传递 任意 UTF-8 数据传递给 JNI 并期望它能够正常工作。

如需获取 String 的 UTF-16 表示法,请使用 GetStringChars。 请注意,UTF-16 字符串不是以零终止的,并且允许使用 \u0000。 因此您需要保留字符串长度和 jchar 指针。

不要忘记 ReleaseGet 的字符串。通过 字符串函数会返回 jchar*jbyte*, 是指向原始数据而非局部引用的 C 样式指针。他们 在调用 Release 之前保证有效,也就是说, 在原生方法返回时释放。

传递给 NewStringUTF 的数据必须采用修改后的 UTF-8 格式。答 常见错误是从文件或网络流中读取字符数据 并将其传递给 NewStringUTF,而不进行过滤。 除非您知道数据是有效的 MUTF-8(或兼容子集,即 7 位 ASCII), 您需要剔除无效字符或将其转换为适当的修改后的 UTF-8 格式。 如果不这样做,UTF-16 转换可能会产生意外的结果。 CheckJNI(模拟器默认处于启用状态)会扫描字符串 并在收到无效输入时中止虚拟机

在 Android 8 之前,使用 UTF-16 字符串操作的速度通常快于 Android 在 GetStringChars 中不需要副本,而 “GetStringUTFChars”需要分配和转换为 UTF-8。 Android 8 将 String 表示法更改为了每个字符使用 8 位 用于 ASCII 字符串(以节省内存),并开始使用 移动 垃圾回收器。这些功能大大减少了 ART 出现故障的情况 可以提供指向 String 数据的指针,而无需复制,即使 价格为 GetStringCritical。不过,如果代码处理的大多数字符串 因此在大多数情况下,可以通过以下方式避免分配和取消分配 使用堆栈分配的缓冲区和 GetStringRegionGetStringUTFRegion。例如:

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr<jchar[]> heap_buffer;
    jchar* buffer = stack_buffer;
    jsize length = env->GetStringLength(str);
    if (length > kStackBufferSize) {
      heap_buffer.reset(new jchar[length]);
      buffer = heap_buffer.get();
    }
    env->GetStringRegion(str, 0, length, buffer);
    process_data(buffer, length);

原始数组

JNI 提供访问数组对象内容的函数。 虽然对象数组每次只能访问一个条目,但 可以直接读取和写入基元,就像使用 C 语言声明它们一样。

使界面尽可能高效,同时又不会受到任何限制 虚拟机实现,Get<PrimitiveType>ArrayElements 一系列调用允许运行时返回指向实际元素的指针,或者 分配一些内存并创建一个副本。无论哪种情况,返回的原始指针 在相应的 Release 调用之前始终有效 (这意味着,如果未复制数据,则数组对象 将被固定,并且无法在压缩堆的过程中重新定位)。 您必须 Release 自己 Get 的每个数组。此外,如果 Get 调用失败,您必须确保代码不会尝试向 Release 返回 NULL 指针。

您可以通过传入 isCopy 参数的非 NULL 指针。很少出现 实用。

Release 调用接受 mode 参数, 具有三个值中的一个。运行时执行的操作取决于 返回指向实际数据还是指向数据副本的指针:

  • 0
    • 实际数据:数组对象未固定。
    • 数据副本:已复制回数据。释放了包含相应副本的缓冲区。
  • JNI_COMMIT
    • 实际数据:不执行任何操作。
    • 数据副本:已复制回数据。包含相应副本的缓冲区 未释放
  • JNI_ABORT
    • 实际数据:数组对象未固定。较早 写入中止。
    • 数据:释放包含相应副本的缓冲区;对该文件所做的任何更改都会丢失。

检查isCopy标志的一个原因是了解 你需要使用 JNI_COMMIT 来调用 Release 更改数组之后的效果 - 如果您在更改数组时交替使用 更改并执行使用数组内容的代码, 能够 则跳过无操作提交检查该标志的另一个可能原因是 高效处理 JNI_ABORT。例如,您可能想 来获取数组,进行适当修改,将部分传递给其他函数,以及 然后舍弃这些更改如果您知道 JNI 正在为 无需创建另一个“可修改”复制。如果 JNI 通过 您需要制作自己的副本。

一种常见的错误是认为您可以跳过 Release 调用(在示例代码中重复出现过这种情况) *isCopy 为 false。事实并非如此。如果 则原始内存必须固定,并且不能 垃圾回收器

另请注意,JNI_COMMIT 标志不会释放数组, 并且您需要使用其他标志再次调用 Release 最终。

区域调用

Get<Type>ArrayElements 等通话外,还有其他通话方式 以及GetStringChars。 就是将数据复制进或出注意事项:

    jbyte* data = env->GetByteArrayElements(array, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(array, data, JNI_ABORT);
    }

此命令会抓取数组,并复制第一个 len 字节 将元素移出该数组,然后释放该数组。根据 实现时,Get 调用会固定或复制数组 内容。 代码复制数据(可能是第二次),然后调用 Release;在本例中 JNI_ABORT 可确保不会出现第三个副本。

您还能够以更简单的方式完成相同操作:

    env->GetByteArrayRegion(array, 0, len, buffer);

这种做法具有诸多优势:

  • 需要一个 JNI 调用而不是两个,从而减少开销。
  • 不需要固定或额外复制数据。
  • 降低程序员出错风险 - 没有遗忘的风险 在发生操作失败后调用 Release

同样,您可以使用 Set<Type>ArrayRegion 调用 将数据复制到数组中;GetStringRegionGetStringUTFRegion,用于从 String

异常

在异常挂起时,不得调用大多数 JNI 函数。 您的代码应该会注意到异常(通过函数的返回值, ExceptionCheckExceptionOccurred)返回, 或清除异常并进行处理。

发生异常时,您只能调用以下 JNI 函数: 的待处理请求包括:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

许多 JNI 调用都会抛出异常,但通常会提供一种更简单的方法 检查是否失败。例如,如果 NewString 返回 非 NULL 值,则无需检查异常。但是,如果 您调用一个方法(使用 CallObjectMethod 等函数), 您必须始终检查异常,因为返回值并非 才有效。

请注意,受管理代码抛出的异常不会展开原生堆栈 帧。(Android 中通常不建议使用 C++ 异常, 从 C++ 代码到托管代码跨 JNI 转换边界抛出。) JNI ThrowThrowNew 指令仅需 在当前线程中设置异常指针。返回到受管理状态后 从原生代码中读取时,系统会记录异常并进行相应处理。

原生代码可以“捕捉”调用 ExceptionCheckExceptionOccurred,然后使用 ExceptionClear。像往常一样 如果舍弃异常而不处理异常,可能会导致问题。

没有用于操控 Throwable 对象的内置函数 所以,如果您想获取异常字符串,则需要 找到 Throwable 类,然后查找 getMessage "()Ljava/lang/String;",调用该函数,如果结果 是非 NULL 值,请使用 GetStringUTFChars 来获取一些 交给 printf(3) 或同等机构。

扩展的检查

JNI 很少进行错误检查。错误通常会导致崩溃。Android 还提供了一种名为 CheckJNI 的模式,其中 JavaVM 和 JNIEnv 函数表指针已切换为在调用标准实现之前执行一系列扩展的检查的函数表。

额外检查包括:

  • 数组:尝试分配大小为负值的数组。
  • 错误指针:将错误的 jarray/jclass/jobject/jstring 传递给 JNI 调用,或者将 NULL 指针传递给带有不可设为 null 的参数的 JNI 调用。
  • 类名称:将类名称的“java/lang/String”样式之外的所有内容传递给 JNI 调用。
  • 关键调用:在“关键”get 及其相应 release 之间调用 JNI。
  • 直接字节缓冲区:将错误参数传递给 NewDirectByteBuffer
  • 异常:在异常挂起时调用 JNI。
  • JNIEnv*:使用错误线程中的 JNIEnv*。
  • jfieldID:使用 NULL jfieldID,或者使用 jfieldID 将字段设置为错误类型的值(例如,尝试将 StringBuilder 分配给 String 字段),或者使用静态字段的 jfieldID 设置实例字段(反之亦然),或者将一个类中的 jfieldID 与另一个类的实例搭配使用。
  • jmethodID:在调用 Call*Method JNI 时使用错误类型的 jmethodID:返回类型不正确、静态/非静态不匹配、“this”类型错误(对于非静态调用)或类错误(对于静态调用)。
  • 引用:对错误类型的引用使用 DeleteGlobalRef/DeleteLocalRef
  • Release 模式:将错误的 release 模式传递给 release 调用(除 0JNI_ABORTJNI_COMMIT 之外的内容)。
  • 类型安全:从原生方法返回不兼容的类型(例如,从声明返回 String 的方法返回 StringBuilder)。
  • UTF-8:将无效的修改后的 UTF-8 字节序列传递给 JNI 调用。

(仍未检查方法和字段的可访问性:访问限制不适用于原生代码。)

您可以通过以下几种方法启用 CheckJNI。

如果您使用的是模拟器,CheckJNI 默认处于启用状态。

如果您使用的是已取得 root 权限的设备,则可以使用以下命令序列重新启动运行时,并启用 CheckJNI:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

在以上任何一种情况下,当运行时启动时,您将在 logcat 输出中看到如下内容:

D AndroidRuntime: CheckJNI is ON

如果您使用的是常规设备,则可以使用以下命令:

adb shell setprop debug.checkjni 1

这不会影响已经运行的应用,但从那时起启动的任何应用都将启用 CheckJNI。(将属性更改为任何其他值,或者只是重新启动应用都将再次停用 CheckJNI。)在这种情况下,当应用下次启动时,您将在 logcat 输出中看到如下内容:

D Late-enabling CheckJNI

您还可以将应用清单中的 android:debuggable 属性设置为 请为您的应用开启 CheckJNI。请注意,对于 特定 build 类型。

原生库

您可以使用标准 API 从共享库加载原生代码 System.loadLibrary

事实上,旧版 Android 的 PackageManager 中存在导致安装和 使原生库更新不可靠。ReLinker 项目提供了解决此问题和其他原生库加载问题的解决方法。

从静态类调用 System.loadLibrary(或 ReLinker.loadLibrary) 初始化函数。参数是“未修饰”库名称 因此,要加载 libfubar.so,您需要传入 "fubar"

如果您只有一个类具有原生方法,则调用 System.loadLibrary 位于该类的静态初始化程序中。否则 您希望从 Application 进行该调用,这样您就知道始终会加载该库, 并且始终会提前加载

运行时可以通过两种方式找到您的原生方法。您可以 请使用 RegisterNatives 注册它们;也可以让运行时动态查询它们 和dlsymRegisterNatives 的优势在于,您可以提前 还可以检查这些符号是否存在 导出除 JNI_OnLoad 之外的任何内容。这样做的好处是让运行时 因为它需要编写的代码略少一些。

如需使用 RegisterNatives,请执行以下操作:

  • 提供 JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) 函数。
  • JNI_OnLoad 中,使用 RegisterNatives 注册所有原生方法。
  • 使用 -fvisibility=hidden 进行构建,以便仅使用您的 JNI_OnLoad 。这样可以生成更快、更小的代码,并避免 与加载到您的应用中的其他库发生冲突(但创建的堆栈轨迹没有多大用处) (如果您的应用在原生代码中崩溃)。

静态初始化程序应如下所示:

Kotlin

companion object {
    init {
        System.loadLibrary("fubar")
    }
}

Java

static {
    System.loadLibrary("fubar");
}

如果存在以下情况,则 JNI_OnLoad 函数应如下所示: 使用 C++ 编写:

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    // Find your class. JNI_OnLoad is called from the correct class loader context for this to work.
    jclass c = env->FindClass("com/example/app/package/MyClass");
    if (c == nullptr) return JNI_ERR;

    // Register your class' native methods.
    static const JNINativeMethod methods[] = {
        {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},
        {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},
    };
    int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
    if (rc != JNI_OK) return rc;

    return JNI_VERSION_1_6;
}

要改用“发现”您需要以特定方式为其命名(请参阅 JNI 规范 了解详情)。这意味着,如果某个方法签名是错误的,在调用 调用该方法。

JNI_OnLoad 进行的任何 FindClass 调用都将解析 用于加载共享库的类加载器的上下文。从其他模式调用时 上下文,FindClass 会使用与位于 Java 堆栈,或者没有 Java 堆栈(因为调用来自刚刚附加的原生线程) 它使用“系统”类加载器。系统类加载器不知道您应用的 类,因此您无法在其中使用 FindClass 查找自己的类 上下文。这使得 JNI_OnLoad 成为查找和缓存类的便捷位置:一次 您具有有效的 jclass 全局引用 你可以在任何附加的线程中使用它

使用 @FastNative@CriticalNative 加快原生调用速度

原生方法可以带有 @FastNative@CriticalNative (但不是两者)来加快托管代码与原生代码之间的转换。不过,这些注释 包含某些行为变化,使用前需要仔细考虑。虽然我们 简要提及这些更改,请参阅相关文档了解详情。

@CriticalNative 注解只能应用于 使用受管对象(在参数或返回值中,或作为隐式 this),并且此 注解会更改 JNI 转换 ABI。原生实现必须排除 函数签名中的 JNIEnvjclass 参数。

执行 @FastNative@CriticalNative 方法时,垃圾信息 集合无法挂起线程以进行基本工作,并且可能会阻塞。请勿使用 为长时间运行的方法添加注解,包括通常很快但一般不受限制的方法。 特别是,代码不应执行重要的 I/O 操作,也不应该获取原生锁, 可能会长时间保留

这些注解是为了方便系统使用而实现的, Android 8 并公开接受了 CTS 测试 API。这些优化可能也适用于 Android 8-13 设备(尽管 没有强有力的 CTS 保证),但只有 Android 12 及更高版本,必须向 JNI RegisterNatives 明确注册 。在 Android 7 上,这些注解会被忽略,ABI 不匹配 的 @CriticalNative 会导致错误的参数编组,并可能发生崩溃。

对于需要这些注解的性能关键型方法,强烈建议 向 JNI RegisterNatives 明确注册方法,而不是依赖于 基于名称的“discovery”原生方法。为获得最佳应用启动性能,建议 在@FastNative@CriticalNative 基准配置文件。从 Android 12 开始 从经过编译的托管方法调用 @CriticalNative 原生方法几乎与 只要所有参数都能放入寄存器中(例如,最多 在 arm64 上为 8 个整数和最多 8 个浮点参数)。

有时,最好将原生方法一分为二, 另一个函数处理慢情况。例如:

Kotlin

fun writeInt(nativeHandle: Long, value: Int) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value)
    }
}

@CriticalNative
external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean

external fun nativeWriteInt(nativeHandle: Long, value: Int)

Java

void writeInt(long nativeHandle, int value) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value);
    }
}

@CriticalNative
static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value);

static native void nativeWriteInt(long nativeHandle, int value);

64 位注意事项

要支持使用 64 位指针的架构,请使用 long 字段而不是 在 Java 字段中存储指向原生结构的指针时返回 int

不支持的功能/向后兼容性

支持所有 JNI 1.6 功能,但以下情况除外:

  • DefineClass 未实现。Android 不使用 Java 字节码或类文件,因此要传入二进制类数据 不起作用。

为了向后兼容旧版 Android,您可能需要 请注意:

  • 动态查找原生函数

    在 Android 2.0 (Eclair) 之前,“$”字符不正确 已转换为“_00024”。正在处理 需要使用显式注册或将 排除内部类以外的原生方法。

  • 分离线程

    在 Android 2.0 (Eclair) 之前,无法使用 pthread_key_create 析构函数,以避免“必须先分离线程 退出”检查。(运行时还会使用 pthread key 析构函数, 所以会得先比赛,看看哪个先被调用。)

  • 弱全局引用

    在 Android 2.2 (Froyo) 之前,没有实现弱全局引用。 旧版本会强烈排斥使用弱全局引用。您可以使用 Android 平台版本常量以测试支持情况。

    在 Android 4.0 (Ice Cream Sandwich) 之前,弱全局引用只能 传递给 NewLocalRefNewGlobalRefDeleteWeakGlobalRef。(该规范强烈建议 程序员创建对弱全局的硬引用 所以这不应该有任何限制。)

    从 Android 4.0 (Ice Cream Sandwich) 开始,弱全局引用可以 像任何其他 JNI 引用一样使用。

  • 局部引用

    在 Android 4.0 (Ice Cream Sandwich)之前,局部引用一直 实际上是直接指针Ice Cream Sandwich 添加了间接 但这也意味着 的 JNI bug 在旧版本中无法检测到。请参阅 <ph type="x-smartling-placeholder"></ph> ICS 中的 JNI 本地引用更改

    Android 8.0 之前的 Android 版本中, 局部引用的数量受到特定于版本的限制。从 Android 8.0 开始, Android 支持无限的局部引用。

  • 使用 GetObjectRefType 确定引用类型

    Android 4.0 (Ice Cream Sandwich) 之前,由于使用 直接指针(见上文),则无法实现 GetObjectRefType正确。我们改为使用启发词语, 其中,参数、局部变量 表和全局表(按该顺序)。它首次发现您的 直接指针,则系统会报告您的引用属于 确实需要检查例如,这意味着 您对之前发生的全局 jclass 调用了 GetObjectRefType, 与作为隐式参数传递给静态变量的 jclass 相同 原生方法,会得到 JNILocalRefType,而不是 JNIGlobalRefType

  • @FastNative@CriticalNative

    在 Android 7 及之前,这些优化注解已被忽略。ABI @CriticalNative 不匹配会导致错误的参数 进行编组处理并可能发生崩溃

    动态查找 @FastNative@CriticalNative 方法在 Android 8-10 中未实现, 包含 Android 11 中的已知 bug。使用这些优化措施 通过 JNI 明确注册 RegisterNatives 可能会导致 会导致崩溃。

  • FindClass 抛出 ClassNotFoundException

    为了向后兼容,Android 会抛出 ClassNotFoundException 而不是 NoClassDefFoundError,因为没有 FindClass。此行为与 Java 反射 API 一致 Class.forName(name)

常见问题解答:为什么我会收到 UnsatisfiedLinkError

在处理原生代码时,经常可以看到如下所示的失败消息:

java.lang.UnsatisfiedLinkError: Library foo not found

在某些情况下,正如字面意思所说 - 找不到库。在 出现此库但无法被 dlopen(3) 打开的其他情况,以及 您可在异常的详情消息中找到失败详情。

您可能遇到“找不到库”异常的常见原因如下:

  • 库不存在或应用无法访问。使用 adb shell ls -l <path>,检查其是否存在 和权限。
  • 库不是使用 NDK 构建的。这可能会导致 对设备上不存在的函数或库的依赖关系。

其他类的 UnsatisfiedLinkError 失败消息如下所示:

java.lang.UnsatisfiedLinkError: myfunc
        at Foo.myfunc(Native Method)
        at Foo.main(Foo.java:10)

在 logcat 中,您将看到以下内容:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

这意味着,运行时尝试查找匹配方法, 失败。造成此问题的一些常见原因如下:

  • 库未加载。检查 logcat 输出, 库加载消息
  • 名称或签名不匹配,因此找不到该方法。本次 通常由以下原因引起: <ph type="x-smartling-placeholder">
      </ph>
    • 对于延迟方法查找,无法声明 C++ 函数 包含 extern "C" 和适当的 可见性 (JNIEXPORT)。请注意,在投放 Ice Cream 之前, Sandwich 中,JNIEXPORT 宏不正确,因此请使用带有 旧的jni.h将无法使用。 您可以使用 arm-eabi-nm 查看符号在库中显示的符号;如果它们 损坏(诸如_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass之类的内容) 而不是 Java_Foo_myfunc),或者如果符号类型是 小写“t”而不是大写的“T”,则需要 调整声明
    • 对于显式注册,在输入 方法签名。请确保您传递到 与日志文件中的签名匹配。 记住“B”为“byte”和“Z”为 boolean。 签名中的类名称组成部分以“L”开头,以“;”结尾, 使用“/”分隔软件包/类名称,然后使用“$”来分隔 内部类名称(比如 Ljava/util/Map$Entry;)。

使用 javah 自动生成 JNI 标头可能会有帮助 可以避免一些问题。

常见问题解答:为什么 FindClass 找不到我的类?

(以下建议的大部分内容同样适用于找不到方法的问题 包含 GetMethodIDGetStaticMethodID 或字段 使用 GetFieldIDGetStaticFieldID。)

确保类名称字符串的格式正确无误。JNI 类 名称以软件包名称开头,并用正斜线分隔 例如 java/lang/String。如果您要查找数组类, 您需要以适当数量的方括号开头 还必须使用“L”封装类和“;”这样的一个一维数组, String[Ljava/lang/String;。 如果您要查找内部类,请使用“$”而不是“.”。一般来说, 对 .class 文件使用 javap 是查找 类的内部名称。

如果要启用代码缩减,请确保 配置要保留的代码。正在配置 适当的保留规则非常重要,因为代码压缩器可能会从别处移除类、方法 或仅通过 JNI 使用的字段。

如果类名称没有问题,则可能是因为您遇到了类加载器 问题。FindClass想要在 与代码关联的类加载器。它会检查调用堆栈, 如下所示:

    Foo.myfunc(Native Method)
    Foo.main(Foo.java:10)

最顶层的方法是 Foo.myfuncFindClass 查找与 Foo 关联的 ClassLoader 对象 类并使用该类。

采用这种方法通常会完成您想要执行的操作。如果您 自行创建线程(可能通过调用 pthread_create) ,然后使用 AttachCurrentThread 附加该映像)。现在 不是来自应用的堆栈帧。 如果您从此线程调用 FindClass, JavaVM 将在“system”下启动类加载器,而不是 与您的应用关联,因此尝试查找特定于应用的类将失败。

您可以通过以下几种方法来解决此问题:

  • FindClass JNI_OnLoad,并缓存类引用以供日后使用 。执行过程中进行的任何 FindClass 调用 JNI_OnLoad 将使用与 调用 System.loadLibrary 的函数(这是 特殊规则,以方便执行库初始化)。 如果您的应用代码要加载库,请FindClass 将使用正确的类加载器。
  • 将类的实例传递到 方法是声明原生方法接受 Class 参数 然后传入 Foo.class
  • 在某处缓存对 ClassLoader 对象的引用 然后直接发出 loadClass 调用。这需要 不费吹灰之力

常见问题解答:如何使用原生代码共享原始数据?

您可能会发现自己需要访问大量 来自受管理代码和原生代码的原始数据的缓冲区。常见示例 包含对位图或声音样本的操纵。有两个 基本方法。

您可以将数据存储在 byte[] 中。这样可让系统 通过托管代码进行访问。而在原生广告方面 访问相应数据,而无需复制数据。在 一些实现,GetByteArrayElementsGetPrimitiveArrayCritical 将返回指向 托管堆中的原始数据,但在其他情况下,系统会分配缓冲区 并复制数据

另一种方法是将数据存储在直接字节缓冲区中。这些 可使用 java.nio.ByteBuffer.allocateDirect 创建,或 JNI NewDirectByteBuffer 函数。不同于普通 字节缓冲区,因此存储空间不会在托管堆上分配,并且可以 始终直接从原生代码中访问(获取 与 GetDirectBufferAddress 相关联)。取决于 已实现字节缓冲区访问,从托管代码访问数据 可能会非常慢

选择使用哪种方法取决于以下两个因素:

  1. 大部分数据访问是通过使用 Java 编写的代码进行的吗 还是用 C/C++ 开发?
  2. 如果数据最终传递到系统 API,以 应该在其中吗?(例如,如果数据最终传递到 函数,它接受一个 byte[] 值,直接 ByteBuffer 可能不太明智。)

如果这两种方法不分伯仲,请使用直接字节缓冲区。为他们提供的支持 直接内置在 JNI 中,性能应该会在未来版本中得到改进。