Android Framework 源码阅读:JNI 调用链

3 阅读3分钟

Android Framework 源码阅读:JNI 调用链

本文以旧版 AOSP 的 JNI 实现为例,跟踪一次 Java native 方法如何进入 C/C++,以及结果如何返回 Java。类名和构建命令会随 Android、JDK、NDK 版本变化,文中的 javah 只代表历史流程。

摘要

JNI 解决的是语言边界,不是进程间通信。Java 代码声明 native 方法,运行时根据静态命名约定或 RegisterNatives() 找到 C/C++ 函数,再通过线程相关的 JNIEnv 完成参数转换、对象访问和异常传递。

JNI 调用示意图

1. 一次 JNI 调用的四个阶段

Java 方法声明 native
  → 虚拟机查找 native 符号
  → C/C++ 函数通过 JNIEnv 访问 Java 对象
  → 返回值转换为 Java 类型

JNI 函数运行在发起调用的 Java 线程上。JNIEnv* 是线程相关的接口指针,不能从一个线程直接拿到另一个线程使用;native 创建的线程需要先 AttachCurrentThread(),退出前再 detach。

2. 静态注册:按命名约定查找

Java 端只需要声明方法:

public final class NativeHelper {
    static {
        System.loadLibrary("native-lib");
    }

    public static native String getAppKey();
}

旧版流程常用 javah 生成头文件。头文件是构建辅助产物,不是 JNI 必需条件;现代 JDK 通常使用 javac -h 或由 Gradle/NDK 构建系统生成声明。

静态查找的核心是 JNI 命名约定:类名、方法名和签名会编码进 native 符号。运行时加载 so 后,根据这个规则解析函数地址。

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_NativeHelper_getAppKey(JNIEnv* env, jclass clazz) {
    return env->NewStringUTF("demo-key");
}

3. 动态注册:JNI_OnLoad 与 RegisterNatives

动态注册把 Java 方法和 C/C++ 函数显式写入 JNINativeMethod[],在 JNI_OnLoad() 中调用 RegisterNatives()

static JNINativeMethod kMethods[] = {
    {"getAppKey", "()Ljava/lang/String;", (void*)GetAppKey},
};

jint JNI_OnLoad(JavaVM* vm, void*) {
    JNIEnv* env = nullptr;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    jclass clazz = env->FindClass("com/example/NativeHelper");
    if (env->RegisterNatives(clazz, kMethods, 1) != 0) return JNI_ERR;
    return JNI_VERSION_1_6;
}

动态注册不要求每个 Java 类生成一个 .h 文件,但每个目标类仍需要在运行时完成注册。多个类可以共用同一个 native 实现文件。

4. 类型转换与引用生命周期

常见转换包括:

  • jstring 与 UTF-8/UTF-16 字符串之间的转换;
  • jobjectArrayjbyteArray 与 native 缓冲区之间的转换;
  • jobjectjclassjmethodID 的查找和调用。

局部引用通常在 JNI 调用返回时自动释放;需要跨越当前调用保存的对象必须创建全局引用,并在不再使用时 DeleteGlobalRef()。调用 Java 方法后要检查 ExceptionCheck(),否则异常可能延迟到下一次 JNI 边界才暴露。

5. native 线程与 JNIEnv

void Worker(JavaVM* vm) {
    JNIEnv* env = nullptr;
    vm->AttachCurrentThread(&env, nullptr);
    // 只在当前线程使用 env
    vm->DetachCurrentThread();
}

JavaVM* 可以在进程内共享,JNIEnv* 不能跨线程共享。这是排查 native 崩溃和“偶现”引用错误时最重要的边界。

6. JNI 与 Binder 的关系

JNI 负责 Java/native 之间的调用和类型转换;Binder 负责进程之间的事务传输。Framework 服务中经常先经过 JNI 进入 native,再由 BpBinder 发起跨进程调用,但两者不是同一层:

Java API → JNI 边界 → native Binder → Binder 驱动 → 远端服务

如果调用没有跨进程,JNI 调用结束后就直接返回;只有拿到远端 Binder 代理并执行 transact() 时,才进入 IPC 链路。

7. 源码核对顺序

  1. 记录 AOSP 分支、JDK、NDK 版本;
  2. 从 Java native 声明或 System.loadLibrary() 进入;
  3. 核对静态符号或 RegisterNatives() 的映射;
  4. 检查 JNIEnv、引用生命周期和异常处理;
  5. 最后确认是否继续进入 Binder 等其他子系统。

本文结论:JNI 是一条受线程和引用规则约束的 Java/native 调用边界;静态注册依赖命名约定,动态注册依赖 JNI_OnLoad() 的显式映射。