Android随笔-JNI

发布时间:2026/7/29 0:13:51
Android随笔-JNI JNIJava Native Interface是 Java 与 Native 代码C/C之间的桥梁。它的调用流程可以从两个方向理解Java → Native 和 Native → Java。下面从应用层到 JVM 底层完整拆解其调用链路。一、Java 调用 Native正向调用1.1 应用层代码// HelloJNI.javapublicclassHelloJNI{// 1. 加载 so 库static{System.loadLibrary(hello);}// 2. 声明 native 方法publicnativeStringsayHello(Stringname);publicstaticvoidmain(String[]args){HelloJNIjninewHelloJNI();// 3. 调用 native 方法Stringresultjni.sayHello(World);System.out.println(result);}}1.2 完整调用流程┌─────────────────────────────────────────────────────────────┐ │ Java 层 (HelloJNI.java) │ │ System.loadLibrary(hello) → sayHello(World) │ └──────────────────────────┬──────────────────────────────────┘ │ JNI 调用 ┌──────────────────────────▼──────────────────────────────────┐ │ JVM 层 (libjvm.so) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ ClassLoader │→│ 链接 so 库 │→│ 符号解析 (dlsym) │ │ │ │ 加载 .so │ │ (JNI_OnLoad) │ │ 查找 Java_com_... │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ └──────────────────────────┬──────────────────────────────────┘ │ ┌──────────────────────────▼──────────────────────────────────┐ │ Native 层 (libhello.so) │ │ JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(...) │ │ ├── 获取 JNIEnv* 指针 │ │ ├── 操作 jstringNewStringUTF / GetStringUTFChars │ │ └── 返回 jstring 给 Java │ └─────────────────────────────────────────────────────────────┘1.3 底层详细步骤步骤 1加载 so 库System.loadLibrary(hello);内部调用链路Runtime.loadLibrary0(ClassLoader, String) → ClassLoader.findLibrary(name) // 查找 libhello.so 路径 → Runtime.nativeLoad(filename, classLoader) // 进入 native → JVM_LoadLibrary() // JVM 层 → dlopen() // 操作系统层加载动态库 → dlsym(handle, JNI_OnLoad) // 查找 JNI_OnLoad 符号 → JNI_OnLoad(JavaVM*, void*) // 执行 so 的初始化步骤 2链接 native 方法当首次调用 sayHello() 时JVM 发现该方法带有 ACC_NATIVE 标志位触发方法链接InterpreterRuntime::resolve_native_call() → NativeLookup::lookup(method) → 方式 A静态注册按命名规则查找 → dlsym(handle, Java_HelloJNI_sayHello) → 方式 B动态注册通过 JNI_OnLoad 注册 → 从 JNINativeMethod 表中查找 → 将方法指针写入 Method::_native_function步骤 3执行 native 方法JVM 通过 JavaCalls::call_helper() 进入 native 代码构建调用栈帧JavaThread::thread_state _thread_in_native // 标记线程状态 // 构建参数调用 native 函数 result (*native_function)(env, jobject, args...) JavaThread::thread_state _thread_in_Java // 恢复线程状态关键数据结构JNIEnv*线程本地指针指向一个函数表 JNINativeInterface包含所有 JNI 函数指针。jobjectJava 对象的 Native 引用实际是 JVM 内部对象句柄的封装。二、Native 调用 Java反向调用2.1 典型场景在 Native 层需要回调 Java 方法例如异步任务完成通知// native 层代码JNIEXPORTvoidJNICALLJava_HelloJNI_startAsyncTask(JNIEnv*env,jobject thiz){// 保存全局引用jclass clazzenv-GetObjectClass(thiz);jmethodID methodIdenv-GetMethodID(clazz,onComplete,(Ljava/lang/String;)V);// 假设异步任务完成后jstring resultenv-NewStringUTF(任务完成);env-CallVoidMethod(thiz,methodId,result);// 回调 Java 方法}2.2 调用流程Native 层 │ ▼ env-FindClass(com/example/HelloJNI) // 查找类 │ ▼ env-GetMethodID(clazz, onComplete, (Ljava/lang/String;)V) // 获取方法 ID │ ▼ env-CallVoidMethod(jobject, jmethodID, args...) // 调用 Java 方法 │ ▼ JVM 层 ├── 检查线程状态_thread_in_native → _thread_in_vm ├── 解析 jmethodID → Method* ├── 构建 Java 调用栈帧 ├── 进入解释器或 JIT 编译后的代码 └── 执行 Java 方法 onComplete()2.3 方法签名规则JNI 使用内部签名格式标识方法Java 类型JNI 签名voidVbooleanZintIlongJStringLjava/lang/String;int[][IObjectLjava/lang/Object;示例voidonComplete(Stringmsg,intcode);// 签名(Ljava/lang/String;I)V三、两种注册方式对比3.1 静态注册命名规则匹配Native 方法名必须遵循固定格式Java_包名_类名_方法名例如packagecom.example;publicclassHelloJNI{nativevoidsayHello();}对应 NativeJNIEXPORTvoidJNICALLJava_com_example_HelloJNI_sayHello(JNIEnv*,jobject);缺点方法名冗长不支持方法重载需加参数签名后缀首次调用时才链接有运行时开销3.2 动态注册推荐在 JNI_OnLoad 中主动注册性能更好// 方法映射表staticJNINativeMethod gMethods[]{{sayHello,(Ljava/lang/String;)Ljava/lang/String;,(void*)native_sayHello},{add,(II)I,(void*)native_add},};JNIEXPORT jint JNICALLJNI_OnLoad(JavaVM*vm,void*reserved){JNIEnv*env;if(vm-GetEnv((void**)env,JNI_VERSION_1_6)!JNI_OK){returnJNI_ERR;}jclass clazzenv-FindClass(com/example/HelloJNI);// 一次性注册所有方法env-RegisterNatives(clazz,gMethods,sizeof(gMethods)/sizeof(gMethods[0]));returnJNI_VERSION_1_6;}优点方法名自由支持重载加载 so 时一次性链接首次调用无延迟四、线程与引用管理4.1 JNIEnv 的线程绑定JNIEnv* 是线程局部存储TLS的不能跨线程传递// ❌ 错误将 JNIEnv 传给其他线程使用voidwrongUsage(JNIEnv*env){std::thread([env](){env-FindClass(...);// 崩溃JNIEnv 不能跨线程}).detach();}// ✅ 正确通过 JavaVM 获取当前线程的 JNIEnvJavaVM*g_vm;// 在 JNI_OnLoad 中保存voidcorrectUsage(){JNIEnv*env;g_vm-AttachCurrentThread(env,nullptr);env-FindClass(...);g_vm-DetachCurrentThread();}4.2 引用类型类型特点使用场景局部引用Local Ref方法返回后自动释放不能缓存临时对象全局引用Global Ref跨方法、跨线程存活需手动释放缓存 jclass/jobject弱全局引用Weak Global Ref不阻止 GC可能随时失效避免内存泄漏// 局部引用 → 全局引用缓存jclass localClazzenv-FindClass(com/example/HelloJNI);jclass globalClazz(jclass)env-NewGlobalRef(localClazz);// 缓存env-DeleteLocalRef(localClazz);// 释放局部引用// 使用完毕后释放全局引用env-DeleteGlobalRef(globalClazz);五、完整流程总结┌────────────────────────────────────────────────────────────────┐ │ JNI 调用全景图 │ ├────────────────────────────────────────────────────────────────┤ │ │ │ Java 层 JVM 层 Native 层 │ │ │ │ │ │ │ │ 1. System.loadLibrary │ │ │ │ │────────────────────────│ dlopen / dlsym │ │ │ │ │───────────────────────│ │ │ │ │ JNI_OnLoad │ │ │ │ │───────────────────────│ │ │ │ │ RegisterNatives │ │ │ │ │ │ │ │ │ 2. 调用 native 方法 │ │ │ │ │────────────────────────│ 查找 Method::_native │ │ │ │ │ 构建栈帧切换线程状态 │ │ │ │ │───────────────────────│ │ │ │ │ 执行 C/C 代码 │ │ │ │ │ 操作 JNIEnv* 访问 Java │ │ │ │ │───────────────────────│ │ │ │ 3. 返回结果 │ 返回 jvalue │ │ │ │────────────────────────│ 恢复线程状态 │ │ │ │ │ │ │ │ │ 4. Native 回调 Java │ │ │ │ │────────────────────────│ CallVoidMethod │ │ │ │ 执行 Java 方法 │ 进入解释器/JIT │ │ │ │ │ │ │ └────────────────────────────────────────────────────────────────┘六、Android 中的特殊注意点ART vs DalvikART 下 JNI 调用性能更好直接调用 compiled codeDalvik 需经过解释器桥接。GC 安全Native 层持有局部引用时JVM 的 GC 可以安全运行全局引用会阻止对象被回收。异常检查Native 层调用 Java 方法后必须检查是否抛出了异常env-CallVoidMethod(obj,methodId);if(env-ExceptionCheck()){env-ExceptionDescribe();// 打印异常堆栈env-ExceptionClear();// 清除异常}so 库路径Android 中 System.loadLibrary() 会从 apk/lib/{abi}/ 目录加载对应 ABI 的 so。