Android的java层如何调用c/c++

0 阅读5分钟

以google官方Android NDK Samples的hello-jni为示例分析

一、 Java 是如何调用 C/C++ 代码的?

整个调用链分为以下 4 个阶段:

  1. 库的加载(触发阶段): 在 HelloJni.kt 的 companion object 静态初始化块中执行: init { System.loadLibrary("hello-jni") } 这会通知 Android 运行时(ART)加载 libhello-jni.so。
  2. 底层 hook 与回调(加载阶段): 当动态库加载进内存后,ART 虚拟机会主动查找动态库中名为 JNI_OnLoad 的导出符号(该符号在 libhello-jni.map.txt 中被显式保留为全局公开)。
  3. 函数表绑定(注册阶段): JNI_OnLoad 内部通过 env->RegisterNatives(...) 将 Kotlin/Java 中的 stringFromJNI() 方法与 C++ 的 StringFromJni(...) 函数指针直接绑定。
  4. 方法执行(调用阶段): 在 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++ 之间能够精确对接,归结于 方法元数据匹配 与 动态链接重定向:

  1. external 关键字与符号标记: 在 Kotlin 中使用 external(对应 Java 的 native),编译器会在生成的 class 字节码中将该方法标记为 ACC_NATIVE。当虚拟机执行该方法时,不会去找字节码指令,而是转而查询本地方法表。
  2. 寻址与建立映射:
  • 动态注册情况(本代码逻辑):通过 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 方法,保持代码生命周期完整闭环。