JNI(Java Native Interface)是 Java 平台的标准编程接口,允许 Java 代码与 C/C++ 等 Native 语言交互。在 Android 系统中,JNI 不仅是应用层 NDK 开发的基础,更是整个 Android 框架运行时的核心——从 Zygote 启动到 SystemServer 初始化,从 View 渲染到 Binder 通信,底层都依赖 JNI 来桥接 Java 与 Native 世界。本文将基于 Android 11 (API 30) 的 AOSP 源码,深入剖析 JNI 的工作原理、注册机制、引用管理和 ART 集成。
一、JNI 在 Android 系统中的地位
1.1 JNI 的两个角色
JNI 在 Android 中扮演两个主要角色:
Java 调用 Native:通过
native关键字声明的方法,由 JNI 解析到对应的 C/C++ 函数实现。如SystemProperties.get()→android_os_SystemProperties.cpp中的 native 函数。Native 调用 Java:Native 层通过 JNI 函数(
FindClass、GetMethodID、CallVoidMethod等)调用 Java 层的方法。如 AMS 通过 JNI 回调 Java 层的 Binder 代理。
1.2 JNI 相关源码路径
art/runtime/jni/ # ART JNI 实现核心 |
二、静态注册 vs 动态注册
2.1 静态注册(Static Registration)
静态注册使用 Java_<包名>_<类名>_<方法名> 的命名约定:
// frameworks/base/core/jni/android_os_SystemProperties.cpp |
当 Java 层调用 SystemProperties.getSS() 时,ART 会自动调用 System.loadLibrary() 的 dlsym 查找这个符号名并绑定。
静态注册的优点:
- 无需额外代码,函数名自动映射
- 适合简单场景,开发方便
静态注册的缺点:
- 函数名极长,不易阅读
- 每次调用都有查找开销(缓存在 ART 中,但首次仍需要)
- 不支持运行时取消绑定
- 对 ProGuard 混淆敏感(类名方法名变化导致链接失败)
2.2 动态注册(Dynamic Registration)
动态注册通过 RegisterNatives 在运行时显式注册 Native 方法。Android 框架广泛使用这种方案:
// frameworks/base/core/jni/android_os_SystemProperties.cpp |
动态注册的入口点通常在 onLoad.cpp 文件中:
// frameworks/base/core/jni/AndroidRuntime.cpp |
这个注册过程发生在 Zygote 的 AndroidRuntime.start() 中,并且因为 Zygote 的 copy-on-write 机制,所有 fork 出的 App 进程自动继承这些注册映射。
三、JNI 类型系统
3.1 基本类型映射
JNI 定义了一套中立于底层硬件平台的类型系统:
| Java 类型 | JNI 类型 | C/C++ 类型 | 字节数 |
|---|---|---|---|
| boolean | jboolean | unsigned char | 1 |
| byte | jbyte | signed char | 1 |
| char | jchar | unsigned short | 2 |
| short | jshort | short | 2 |
| int | jint | int | 4 |
| long | jlong | long long | 8 |
| float | jfloat | float | 4 |
| double | jdouble | double | 8 |
| void | void | void | N/A |
3.2 引用类型
| Java 类型 | JNI 类型 | 说明 |
|---|---|---|
| Object | jobject | 任意 Java 对象 |
| Class | jclass | java.lang.Class |
| String | jstring | java.lang.String |
| Object[] | jobjectArray | 对象数组 |
| boolean[] | jbooleanArray | boolean 数组 |
| byte[] | jbyteArray | byte 数组 |
| char[] | jcharArray | char 数组 |
| short[] | jshortArray | short 数组 |
| int[] | jintArray | int 数组 |
| long[] | jlongArray | long 数组 |
| float[] | jfloatArray | float 数组 |
| double[] | jdoubleArray | double 数组 |
| Throwable | jthrowable | 异常对象 |
3.3 JNI 方法签名(Type Signature)
JNI 使用紧凑的字符编码来表示 Java 类型签名:
| Java 类型签名 | JNI 签名 | 含义 |
|---|---|---|
| Z | boolean | 布尔 |
| B | byte | 字节 |
| C | char | 字符 |
| S | short | 短整型 |
| I | int | 整型 |
| J | long | 长整型 |
| F | float | 单精度浮点 |
| D | double | 双精度浮点 |
| V | void | 空返回 |
| LclassName; | 对象 | 如 Ljava/lang/String; |
| [type | type[] | 如一维 int 数组 [I |
| (arg)ret | 方法 | 如 (ILjava/lang/String;)V |
示例:
// Java: public native String getMessage(int code, byte[] data, String tag); |
四、JNI 引用管理:Local、Global、Weak Global
JNI 引用的管理是 JNI 编程中最容易出错的领域。理解这三种引用的区别和限制至关重要。
4.1 Local Reference(本地引用)
本地引用在 native 方法执行期间有效,方法返回后自动释放。
// 本地引用示例 |
关键限制:每个 JNI 方法调用中,本地引用表的默认容量为 512(Android 实现)。超过此限制会触发 JNI ERROR (app bug): local reference table overflow 崩溃。
// ❌ 错误:在循环中创建大量本地引用而不释放 |
ART 中的本地引用表实现:
// art/runtime/indirect_reference_table.h |
4.2 Global Reference(全局引用)
全局引用在显式删除前一直有效,不受 native 方法返回的影响:
jclass g_my_class = nullptr; |
全局引用表的特点:
- 没有固定容量限制(受进程内存限制)
- 全局引用阻止 GC 回收被引用的对象 → 可能导致内存泄漏
- 使用
NewGlobalRef()创建的每个引用都需要对应的DeleteGlobalRef()
4.3 Weak Global Reference(弱全局引用)
弱全局引用不阻止 GC 回收对象,但需要在使用前检查对象是否存活:
jclass g_weak_class = nullptr; |
4.4 PushLocalFrame / PopLocalFrame
为了管理嵌套调用中的本地引用,JNI 提供了本地栈帧机制:
JNIEXPORT void JNICALL Java_MyClass_processBatch(JNIEnv* env, jclass clazz, |
五、JNI 函数表结构
5.1 JNINativeInterface 结构体
JNIEnv* 是一个指向 JNINativeInterface 函数表指针的指针。这个表包含大约 230 个函数指针:
// art/runtime/jni/jni_internal.h |
调用 JNI 函数的过程:
// Java_com_example_MyClass_nativeMethod(JNIEnv* env, ...) { |
5.2 Android 特化:JNIEnvExt
Android 在标准 JNINativeInterface 的基础上扩展了 JNIEnvExt,增加了一些 Android 特有的功能:
// art/runtime/jni/jni_env_ext.h |
六、从 Native 调用 Java
6.1 调用流程
从 C/C++ 代码调用 Java 方法需要以下步骤:
// 示例:调用 Java 对象的 void onResult(boolean success, String message) 方法 |
6.2 FindClass 在不同线程中的区别
// 普通 JNI 方法中(由 Java 调用进入):可以直接 FindClass |
FindClass 使用调用者的 ClassLoader 来查找类。在 native 线程中,初始状态下没有 ClassLoader,所以 FindClass 只能找到 bootstrap classpath 中的类(如 java.lang.*)。框架类(android.*)需要从 ClassLoader 获取:
// 获取 Android 框架类的方法 |
6.3 方法 ID 缓存
GetMethodID 和 GetFieldID 的性能开销较大,因为它们需要进行字符串哈希和比较。最佳实践是在类加载时缓存这些 ID:
// 全局缓存 |
七、ART 中的 JNI 调用路径
7.1 Java → Native 调用路径
当一个 Java native 方法被调用时,ART 的执行流程:
Java: MyClass.myNativeMethod() |
具体实现:
// art/runtime/entrypoints/quick/quick_jni_entrypoints.cc |
7.2 Native → Java 调用路径
C: env->CallVoidMethod(obj, methodId, args) |
关键性能开销:
FindClass:类名字符串查找,涉及 class table 哈希查找GetMethodID:方法名和签名查找,涉及 ArtMethod 哈希查找Call*Method:参数类型转换(JNI types → ART internal types)
八、异常处理
8.1 检查 JNI 异常
大多数 JNI 函数在出错时不会立即 crash,而是在 JNIEnv 中设置 pending exception。Native 代码必须在调用可能失败的 JNI 函数后立即检查异常:
JNIEXPORT void JNICALL Java_MyClass_callJava(JNIEnv* env, jobject thiz) { |
8.2 从 Native 抛出 Java 异常
JNIEXPORT void JNICALL Java_MyClass_validateInput(JNIEnv* env, jobject thiz, |
九、CheckJNI:调试模式
9.1 开启 CheckJNI
CheckJNI 是 ART 的一个严格检查模式,会在 JNI 调用时进行额外验证:
# 对特定应用开启 |
9.2 CheckJNI 检查的内容
// art/runtime/jni/check_jni.cpp |
开启 CheckJNI 的性能开销大约是 5-10%,主要用在开发阶段检测 JNI 使用错误。
十、JNI_OnLoad 和 JNI_OnUnload
10.1 JNI_OnLoad
当 System.loadLibrary() 加载一个 native 库时,ART 会查找并调用库中的 JNI_OnLoad 函数。这是执行初始化(如动态注册 native 方法)的最佳时机:
// 典型的 JNI_OnLoad 实现 |
10.2 JNI_OnUnload
当 ClassLoader 被 GC 回收时,ART 会调用 JNI_OnUnload(在 Android 中很少发生,因为大多数 native 库与进程生命周期绑定):
void JNI_OnUnload(JavaVM* vm, void* reserved) { |
十一、核心面试题
Q1:静态注册和动态注册的内部查找机制有什么不同?为什么 Android 框架倾向于使用动态注册?
答:静态注册在第一次调用时由 ART 通过 dlsym 查找 Java_<包名>_<类名>_<方法名> 符号,解析成功后缓存在 ArtMethod 结构中。动态注册通过 RegisterNatives 直接建立 ArtMethod → native 函数指针的映射,跳过了符号查找过程。Android 框架偏好动态注册的原因:(1) 数百个 JNI 模块,静态注册的函数名极长且易冲突;(2) 启动性能:所有方法在 Zygote 启动阶段一次性注册完毕,避免了懒加载的累计开销;(3) ProGuard/R8 混淆兼容性:类名和方法名可能被混淆,但动态注册使用的是固定的方法名字符串;(4) 更灵活的版本管理:可以条件判断不同 API level 注册不同方法。
Q2:本地引用表溢出(local reference table overflow)的根因是什么?如何诊断和修复?
答:根因是 JNI 方法在一个调用栈帧中分配的本地引用数量超过了 512 的限制(ART 默认值)。ART 在每次 CallVoidMethod/FindClass/NewObject 等操作时,会在 IndirectReferenceTable 中插入一个新条目。这些引用在 JNI 方法返回时自动释放,但如果在方法内循环创建大量引用而不手动 DeleteLocalRef,就会在方法返回前溢出。这可以视为一种限定范围内的内存泄漏。诊断方法:开启 CheckJNI (dalvik.vm.checkjni=true) 会在溢出时输出详细 trace。修复方法:(1) 在循环内调用 DeleteLocalRef;(2) 使用 PushLocalFrame/PopLocalFrame 管理生命周期;(3) 将大循环拆分为多个 JNI 调用。
Q3:GetStringUTFChars 和 GetStringCritical 的区别是什么?为什么后者需要更谨慎地使用?
答:GetStringUTFChars 返回 UTF-8 编码的字符串副本(可能有内存分配和转换开销),调用期间允许 GC 运行,JVM 可能会暂停 native 代码执行。GetStringCritical 直接返回指向 Java String 内部缓冲区的指针(零拷贝),但其约束严格得多:(1) “critical region” 内不能调用任何可能阻塞或分配内存的 JNI 函数(包括 FindClass、NewStringUTF 等);(2) critical region 应尽量短;(3) 不能嵌套 critical region。GetStringCritical 的主要优势是避免内存分配和 UTF-8 转换开销,适用于简单快速的字符检测场景。在 ART 实现中,GetStringCritical 实际上仍然会进行 UTF-8 转换(因为 ART 内部使用 Modified UTF-8),所以性能差异不像 HotSpot 那么明显。
Q4:为什么 native 线程在调用 FindClass 之前必须调用 AttachCurrentThread?ART 在 attach 过程中做了什么?
答:ART 的 JNIEnv 是线程局部的(thread-local),通过 art::Thread 对象维护。一个 native(pthread)线程在创建时没有被 ART 识别——它没有关联的 art::Thread 对象、没有 JNIEnv、没有 ClassLoader 上下文。AttachCurrentThread 会:(1) 创建一个 art::Thread 对象;(2) 分配一个 JNIEnvExt(包括本地引用表等);(3) 将线程注册到 ART 的 GC 系统中,这样 GC 才能在此线程的栈上遍历 JNI 本地引用;(4) 设置线程的 ClassLoader 为 null(bootstrap classpath only),因此只能找到 java.lang.* 等核心类。如果需要访问框架类,需要手动通过已知对象获取 ClassLoader 并用其 loadClass 方法。
Q5:JNI_OnLoad 中缓存 jmethodID 是安全的吗?如果 ClassLoader 卸载该类重新加载,缓存的 ID 会失效吗?
答:在 Android 的默认 ClassLoader 策略下,一个类一旦被加载就不会被卸载(除了极端的内存压力情况,且即便是 ART 的类卸载也极其罕见)。jmethodID 本质上是 ArtMethod* 指针,只要类本身不被 GC 回收,这个指针就持续有效。因此缓存 jmethodID 在 Android 中是安全且推荐的做法。但如果库被多个 ClassLoader 加载(如插件化框架),同一个类可能有多个 ClassLoader 副本,此时缓存的 ID 只对应一个 ClassLoader 中的版本。另外需要注意:对于 jclass 不应该缓存普通的本地引用,应该使用 NewGlobalRef 将其提升为全局引用。
AOSP 核心路径参考:
art/runtime/jni/jni_internal.h— JNINativeInterface 函数表art/runtime/jni/jni_env_ext.h— JNIEnvExt 扩展定义art/runtime/jni/check_jni.cpp— CheckJNI 实现art/runtime/indirect_reference_table.h— 引用表实现art/runtime/entrypoints/quick/quick_jni_entrypoints.cc— JNI 快速入口art/runtime/entrypoints/jni/jni_entrypoints.cc— JNI 通用入口libnativehelper/include/nativehelper/jni.h— JNI 辅助宏和方法frameworks/base/core/jni/AndroidRuntime.cpp— Android JNI 注册框架frameworks/base/core/jni/android_os_SystemProperties.cpp— 动态注册范例







