以google官方Android NDK Samples的hello-jni为示例分析
一、 Java 是如何调用 C/C++ 代码的?
整个调用链分为以下 4 个阶段:
- 库的加载(触发阶段): 在 HelloJni.kt 的 companion object 静态初始化块中执行: init { System.loadLibrary("hello-jni") } 这会通知 Android 运行时(ART)加载 libhello-jni.so。
- 底层 hook 与回调(加载阶段): 当动态库加载进内存后,ART 虚拟机会主动查找动态库中名为 JNI_OnLoad 的导出符号(该符号在 libhello-jni.map.txt 中被显式保留为全局公开)。
- 函数表绑定(注册阶段): JNI_OnLoad 内部通过 env->RegisterNatives(...) 将 Kotlin/Java 中的 stringFromJNI() 方法与 C++ 的 StringFromJni(...) 函数指针直接绑定。
- 方法执行(调用阶段): 在 HelloJni.onCreate 中调用 stringFromJNI() 时,ART 虚拟机直接根据前一步建立的函数指针映射关系跳转到 StringFromJni 执行,获得返回的 jstring 并渲染至 TextView。
二、 hello-jni.cpp 的动态注册语法与规则
代码中使用的是 JNI 动态注册(Dynamic Registration),避免了传统静态注册(Java_com_example_hellojni_HelloJni_stringFromJNI)那冗长且容易出错的函数名。
1. 核心数据结构:JNINativeMethod
动态注册的核心是定义一个 JNINativeMethod 结构体数组:
static const JNINativeMethod methods[] = {
{"stringFromJNI", "()Ljava/lang/String;", reinterpret_cast<void*>(StringFromJni)},
};
该结构体包含三个字段:
-
name ("stringFromJNI"): Java/Kotlin 层声明的 native 方法名。
-
signature ("()Ljava/lang/String;"): JNI 方法签名,规则为 (参数类型列表)返回值类型。
-
() 表示无参数。
-
Ljava/lang/String; 表示返回类型为 java.lang.String(完整类名,以 L 开头,以 ; 结尾)。
-
fnPtr (reinterpret_cast<void*>(StringFromJni)): 指向 C++ 本地函数实现的指针,类型被强转为 void*。
2. 实现规范与规则
- JNI_OnLoad 签名: 必须是 extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved),防止 C++ 编译器对其做 Name Mangling(名称修饰),确保系统能通过函数名找到它。
- 返回值: 必须返回期望的 JNI 版本(如 JNI_VERSION_1_6)。如果返回 JNI_ERR 或不支持的版本,ART 将立即终止库的加载并抛出错误。
- C++ 本地实现函数的签名: jstring StringFromJni(JNIEnv* env, jobject thiz)
- 第 1 个参数必须是 JNIEnv*(提供 JNI 函数调用接口)。
- 第 2 个参数:如果 Java 端是普通实例方法(如本例中的 external fun),传入代表当前实例的 jobject;如果 Java 端是静态方法(external fun 定义在单例或带 @JvmStatic),则为代表类对象的 jclass。
- 注册调用: env->RegisterNatives(c, methods, arraysize(methods)); arraysize(methods) 是 Google Base 库提供的宏,等价于 sizeof(methods) / sizeof(methods[0]),计算注册的方法个数。
三、 Java 是如何感知、识别 C/C++ 代码的?
Java/Kotlin 与 C/C++ 之间能够精确对接,归结于 方法元数据匹配 与 动态链接重定向:
- external 关键字与符号标记: 在 Kotlin 中使用 external(对应 Java 的 native),编译器会在生成的 class 字节码中将该方法标记为 ACC_NATIVE。当虚拟机执行该方法时,不会去找字节码指令,而是转而查询本地方法表。
- 寻址与建立映射:
- 动态注册情况(本代码逻辑):通过 RegisterNatives,ART 会在类 com.example.hellojni.HelloJni 的内部结构体中,将 stringFromJNI 的本地方法入口地址(entry point)直接重写为 C++ StringFromJni 的内存地址。
- 未注册情况(如 unimplementedStringFromJNI):调用时由于本地方法表为空,ART 虚拟机尝试在已加载的 .so 的导出符号表中按命名规范自动搜索(如 Java_com_example_hellojni_HelloJni_unimplementedStringFromJNI)。搜索失败后,ART 会抛出 java.lang.UnsatisfiedLinkError。
3.版本/环境隔离(JNIEnv): ART 为每个线程维护了一个独立的 JNIEnv*,Java 到 C++ 的上下文转换由它充当桥梁,从而使得 C++ 能够操作 Java 对象、生成字符串等。
四、 生成的是动态库还是静态库?是怎么生成的?
1. 库类型:动态库(Shared Library,.so 文件)
- 生成物是 动态库(如 libhello-jni.so)。
- 在 Android 中,Java/ART 的 System.loadLibrary 只能动态加载 .so 动态库,无法直接加载 .a 静态库。
2. 生成过程与配置解析:
CMakeLists.txt 中的关键配置如下:
add_app_library(hello-jni SHARED
hello-jni.cpp
)
- 构建指令:SHARED 明确指定生成共享动态库。add_app_library 是基于 add_library 封装的自定义 CMake 函数(通常来自 AppLibrary.cmake)。
- 链接控制(符号隐藏): 注意工程里的 libhello-jni.map.txt,这是一个 Version Script / Symbol Map,它在链接阶段起到了安全与优化作用: LIBHELLOJNI { global: JNI_OnLoad; # 仅向外公开 JNI_OnLoad 这一个符号 local: *; # 隐藏其余所有内部函数(包括 StringFromJni) }; 这保证了生成的 .so 库只有 JNI_OnLoad 可见,其他函数不仅无法被外部逆向工具直接识别符号,还缩小了库体积。
五、 解决方案与最佳实践建议
结合上述代码,如果你需要在实际项目中维护或扩展此类工程,推荐采纳以下解决方案与优化实践:
1. 规范链接器脚本配置(CMakeLists.txt 优化)
当前工程提供了 libhello-jni.map.txt,需要在 CMake 中显式传入链接参数才能生效。建议在 CMakeLists.txt 中添加版本控制脚本链接选项:
target_link_options(hello-jni PRIVATE
"-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/libhello-jni.map.txt"
)
2. 防范混淆导致动态注册失效(ProGuard / R8 规则)
env->FindClass("com/example/hellojni/HelloJni") 使用了硬编码字符串。如果在 Release 打包时开启了代码混淆(Minify),类名或方法名被混淆,会导致 FindClass 失败直接引发崩溃。
- 解决方案:在 proguard-rules.pro 中为相关类和 native 方法配置 keep 规则: -keepclassmembers class com.example.hellojni.HelloJni { native ; } -keep class com.example.hellojni.HelloJni { *; }
3. 释放局部引用
在 JNI_OnLoad 中获取的 jclass c 是一个 Local Reference(局部引用)。虽然在 JNI_OnLoad 执行完毕后会自动释放,但如果有较为繁重的逻辑,良好习惯是手动处理引用或使用全局引用。如果需要在运行期保存该类,应使用:
jclass globalC = (jclass)env->NewGlobalRef(c);
4. 提供注销支持(JNI_OnUnload)
当动态库在极特殊情况下被 ClassLoader 卸载时,应通过 env->UnregisterNatives(c) 注销注册的 native 方法,保持代码生命周期完整闭环。