JNI(一):nativePollOnce() 到底去了哪里?

JNI(一):nativePollOnce() 到底去了哪里?

讲完 Handler 的 Java 消息循环,会留下一个很不自然的断点:

java 复制代码
Message next() {
    final long ptr = mPtr;
    int nextPollTimeoutMillis = 0;

    for (;;) {
        nativePollOnce(ptr, nextPollTimeoutMillis);
        // 回到 Java,重新计算 timeout 并检查消息
    }
}

这里的 ptr 不是另一个字段,而是本次 next() 调用开始时从 mPtr 缓存下来的 Native 句柄;它怎样关联 C++ 对象留到第二篇解释。

nativePollOnce() 只有声明,没有 Java 方法体。它去了哪里?

很多文章会从这里直接跳到 C++ 的 Looper::pollOnce(),再跳到 epoll_wait()。调用链虽然列出来了,中间最重要的几个问题却被略过了:

  • Java 方法凭什么能找到某个 C++ 函数?
  • 从 Java 进入 C++,进程、线程、地址空间有没有变化?
  • 执行到 Native 是否就等于进入内核?
  • JNIEnv*jobjectjlong ptr 分别是谁?
  • JNI、NDK、Binder 为什么经常一起出现,却不是一回事?

这三篇不按"类型表---API 表---引用表"的传统顺序讲 JNI,而是沿着 Handler 留下的问题继续向下追。

本文先只解决一件事:

当前线程执行 MessageQueue.nativePollOnce() 时,怎样通过 JNI 找到 C++ 实现,并继续在同一个线程中运行?

下一篇再回答 mPtr 怎样关联 Java MessageQueueNativeMessageQueue,第三篇完成等待、唤醒闭环,并说明 JNI 到哪里结束、Binder 从哪里开始。

一、先给结论:跨 JNI,跨的不是进程

把这次调用放进 Android 的三层边界,最容易看清:

text 复制代码
Managed Code
Java / Kotlin,运行在 ART 管理环境中
        │
        │ JNI:跨运行时、跨语言调用边界
        ▼
Native Code
C / C++ 编译出的用户态机器码
        │
        │ system call:跨用户态 / 内核态边界
        ▼
Linux Kernel
线程调度、epoll、Binder driver 等

调用 nativePollOnce() 时,通常有四个"不变"和一个"变化"。

不变的是:

text 复制代码
进程不变
地址空间不变
Linux 线程不变
调用关系仍然是同步调用

变化的是:

text 复制代码
当前线程执行的代码,从 ART 管理的 Managed Code 进入了 Native Code。

所以,更准确的描述不是:

当前 Looper 线程把工作交给了一个 Native 线程。

而是:

当前 Looper 线程自己跨过 JNI 边界,继续执行 C++ 函数;C++ 返回以后,同一线程再回到 Java 调用点。

Android 官方 JNI 文档也强调,Java 线程和 C++ 创建的线程底层都是 Linux 线程。Java ThreadNative Thread 不是两种物理线程,"Managed / Native"描述的是代码运行环境,不是线程种类。

只有 Native 代码主动创建线程、切到线程池或异步投递任务时,线程才会发生变化。JNI 本身不会自动创建线程。

二、native 只声明"实现不在 Java",没有说明实现在哪里

Android 16 25Q4 / QPR2 源码快照中的 MessageQueue 声明了多个 native 方法。与本文创建、等待、唤醒和销毁主线直接相关的是下面四个:

java 复制代码
private static native long nativeInit();
private static native void nativeDestroy(long ptr);
private native void nativePollOnce(long ptr, int timeoutMillis);
private static native void nativeWake(long ptr);

同一版本还注册了 nativeIsPollingnativeSetFileDescriptorEventsnativeSetSkipEpollWaitForZeroTimeout;它们不属于本文这条四方法主线。

源码位置:LegacyMessageQueue/MessageQueue.java

这里需要先拆掉一个误解:

native 关键字不会根据 Java 方法名自动生成 C++ 实现。

native 关键字只说明方法体位于 Managed Code 之外,并不直接给出 C/C++ 入口。实际入口可以由 VM 按 JNI 符号命名规则查找,也可以由 Native 代码通过 RegisterNatives() 显式绑定。

JNI 常见的绑定方式有两种。

1. 名字查找,常被中文资料称为"静态注册"

C/C++ 导出符合 JNI 命名规则的符号,VM 在需要解析 native 方法时按规则寻找入口,例如:

cpp 复制代码
Java_包名_类名_方法名(...)

上面是短名字形式。若同一个类中存在同名的 native 重载方法,JNI 还定义了带 __转义后的参数描述符 的长名字:

text 复制代码
短名:Java_包名_类名_方法名
长名:Java_包名_类名_方法名__转义后的参数描述符

VM 名字查找时先尝试短名;找不到时,再尝试包含参数签名的长名。同一个类中有多个同名 native 方法时,需要用长名区分不同参数列表;仅与非 native Java 方法重名时则不需要。完整转义规则见 Oracle JNI 规范:Resolving Native Method Names

这种方式适合演示 Hello JNI,但不是阅读 Android Framework JNI 的主路径。

2. RegisterNatives,也常被称为动态注册

Native 代码明确提供三项信息:

text 复制代码
Java 方法名
+ JNI 方法签名
+ C++ 函数地址

Android Framework 大量使用这种方式。MessageQueue 的注册表就是最直接的例子:

cpp 复制代码
static const JNINativeMethod gMessageQueueMethods[] = {
    // 仅节选本文相关的四项
    {"nativeInit",       "()J",  (void*)android_os_MessageQueue_nativeInit},
    {"nativeDestroy",    "(J)V", (void*)android_os_MessageQueue_nativeDestroy},
    {"nativePollOnce",   "(JI)V",(void*)android_os_MessageQueue_nativePollOnce},
    {"nativeWake",       "(J)V", (void*)android_os_MessageQueue_nativeWake},
};

源码位置:android_os_MessageQueue.cpp

因此,Java 方法和 C++ 函数并不需要同名。在 MessageQueue 采用的 RegisterNatives() 动态注册路径中,真正决定对应关系的是注册表。

三、(JI)V 不是暗号,而是方法形状

nativePollOnce 这一项可以拆成:

text 复制代码
Java 方法名
nativePollOnce

JNI 签名
(JI)V

C++ 函数地址
android_os_MessageQueue_nativePollOnce

其中 (JI)V 表示:

text 复制代码
( J I ) V
  │ │   └─ 返回值 void
  │ └───── 参数 int
  └─────── 参数 long

这里的 J 表示 Java longI 表示 Java intV 表示 void

签名不是文档备注,而是绑定身份的一部分。方法名一样但参数不同,就是不同的方法;签名写错,ART 就无法把 Java 声明和 Native 实现正确对应起来。

本文只拆这一条签名,因为它足以解释 AOSP 注册表。完整类型表属于查询资料,不应该打断主线。

四、注册表怎样真正生效?

有了 JNINativeMethod 数组还不够。承载这套 Framework JNI 的 AndroidRuntime 启动链必须在 VM 创建完成后、进入 Java 主入口前调用 startReg(),从而执行 register_android_os_MessageQueue()

cpp 复制代码
int register_android_os_MessageQueue(JNIEnv* env) {
    // 省略与主线无关的字段、方法 ID 缓存
    return RegisterMethodsOrDie(
            env,
            "android/os/MessageQueue",
            gMessageQueueMethods,
            NELEM(gMessageQueueMethods));
}

register_android_os_MessageQueue() 又会被放进 AndroidRuntime.cppgRegJNI

cpp 复制代码
static const RegJNIRec gRegJNI[] = {
    // ...
    REG_JNI(register_android_os_MessageQueue),
    // ...
};

Android Runtime 启动时会处理这张总表,执行 register_android_os_MessageQueue()。正文只需要保留下面这条主链:

text 复制代码
AndroidRuntime::start()
        ↓
startVm()
        ↓
onVmCreated()
        ↓
startReg(env)
        ↓
register_android_os_MessageQueue(env)
        ↓
RegisterNatives 建立 Java 方法与 C++ 地址的映射
        ↓
调用 app_process 指定的 Java 入口
├─ zygote 模式:ZygoteInit.main()
│       ↓
│   fork system_server;进入 select loop 处理 App fork
└─ 非 zygote 模式:RuntimeInit.main()

源码顺序可以在 AndroidRuntime::start() 中直接核对。app_process 在 zygote 模式下把入口指定为 ZygoteInit;随后 ZygoteInit.main() fork system_server,并进入 zygote server 的 select loop 处理普通应用的 fork 请求。

因此,在典型 Android 应用启动路径中,这套 Framework JNI 注册首先在 zygote 的 app_process 初始化阶段完成;后续 fork 出的 system_server 和应用进程继承已经创建的 Runtime 状态与 JNI 注册映射,并不会为每个应用重新执行一遍 AndroidRuntime::startReg()。非 zygote 模式下,app_process 才会把入口指定为 RuntimeInit.main()

AndroidRuntime 不是 ART 的另一个名字;这里可以把它理解成 Android Framework Native 启动层中的组织者。ART 负责运行 Managed Code,并在后续 native 方法调用时使用这些注册映射。

于是,当消息循环真正执行到 nativePollOnce() 时,ART 已经知道该进入哪个 C++ 函数。

这里也能看出 Framework JNI 与普通 App 教程的差异:这组 JNI 实现位于 app_process 依赖的 libandroid_runtime 中,相关代码会由动态链接器随进程依赖载入地址空间;VM 创建完成后,AndroidRuntime::startReg() 再统一建立 Framework Java native 方法与这些函数地址的映射。因此,Framework 类不需要各自在 Java 代码中调用 System.loadLibrary("xxx")

所以"动态注册"不等于"不需要加载 Native Library":库加载解决"函数代码怎样进入进程",RegisterNatives 解决"Java 方法对应哪个函数地址",责任主体和发生阶段都不同。AndroidRuntime::startReg()register_jni_procs() 的完整批量注册实现可在 AndroidRuntime.cpp 中继续追踪,不再展开成本文主线。

五、进入 C++ 后,四个参数分别是谁?

注册表把调用送到:

cpp 复制代码
static void android_os_MessageQueue_nativePollOnce(
        JNIEnv* env,
        jobject obj,
        jlong ptr,
        jint timeoutMillis) {
    NativeMessageQueue* nativeMessageQueue =
            reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

Java 声明只有两个显式参数:

java 复制代码
nativePollOnce(long ptr, int timeoutMillis)

C++ 为什么有四个?前两个由 JNI 调用约定补入。

JNIEnv* env

JNIEnv* 是当前已附着线程访问 JNI 函数的一组接口,也是与当前线程相关的 JNI 上下文。

通过它可以查类、读字段、调用 Java 方法、创建引用、检查或抛出异常。

这里先记住最重要的一条:

JNIEnv* 与当前线程绑定,不能把线程 A 的 JNIEnv* 直接交给线程 B 使用。

nativePollOnce() 是由已经运行在 ART 中的当前 Looper 线程调用,因此该线程已经附着到 ART,不需要再次调用 AttachCurrentThread()

jobject obj

因为 nativePollOnce 是实例方法,第二个隐藏参数是调用它的 Java MessageQueue 对象,作用近似 Java 里的 this

如果方法是 static native,对应位置通常是 jclass,表示声明该方法的 Java 类。

所以同一个文件里:

text 复制代码
nativePollOnce 不是 static → 第二个参数是 jobject
nativeWake 是 static       → 第二个参数是 jclass

jlong ptr

它来自 Java 的 long 参数,但语义不是普通业务数字。这里保存的是 Native NativeMessageQueue 对象的地址值。

这正是下一篇的主角:谁创建这个对象?为什么 Java 只保存一个 long?谁负责销毁?

jint timeoutMillis

它对应 Java int,是 Java MessageQueue 交给 Native Looper 的等待上限输入,而不是最终 epoll_wait() timeout:

text 复制代码
0
Java 侧要求本轮不等待,立即检查。

正数
Java Message 最多允许等待指定毫秒;
Native Looper 仍可能因 Native Message 或 fd 事件提前返回。

-1
Java 侧当前没有 Message 截止时间;
Native Looper 仍可能根据 Native Message 设置更短 timeout,
也可能被普通 fd、wake eventfd 或信号提前唤醒。

因此,调用 nativePollOnce() 不等于本轮一定阻塞;第三篇会继续追踪 Native Looper 怎样合并自己的截止时间。

六、一次调用怎样从 Java 走到 Native Looper?

现在用一条同线程调用栈验证前置结论:

text 复制代码
同一个 Linux 线程 T

Managed frames
├─ Looper.loop()
├─ MessageQueue.next()
└─ MessageQueue.nativePollOnce(ptr, timeout)
          ↓ ART 使用已注册映射
JNI transition
          ↓
Native frames
├─ android_os_MessageQueue_nativePollOnce()
├─ NativeMessageQueue::pollOnce()
└─ Looper::pollOnce()

NativeMessageQueue::pollOnce() 的关键代码很短:

cpp 复制代码
void NativeMessageQueue::pollOnce(
        JNIEnv* env, jobject pollObj, int timeoutMillis) {
    mPollEnv = env;
    mPollObj = pollObj;
    mLooper->pollOnce(timeoutMillis);
    mPollObj = NULL;
    mPollEnv = NULL;
    // 处理回调期间记录的异常
}

源码位置:NativeMessageQueue::pollOnce()

这里没有创建线程或异步转交,只是当前调用栈从 Managed frame 延伸到 Native frame。再向下才会进入:

text 复制代码
Looper::pollOnce()
        ↓
Looper::pollInner()
        ↓
epoll_wait()

真正可能让线程进入内核等待的是 epoll_wait(),不是 JNI。若 C++ 同步执行过久,发起调用的 Java 线程也会一直被占用。

七、三个相邻概念怎样摆正?

text 复制代码
JNI:Managed Code 与 Native Code 的调用接口;不是 IPC。
NDK:主要面向应用开发者的 Native 工具链和公开 API;可以用于编写 JNI 或纯 Native 代码。AOSP Framework 的 C++ 同样属于 Native Code,但由平台构建系统编译,并且可以使用不属于公开 NDK API 的平台内部接口。
Kernel:要经过 system call 才进入;执行到用户态 C++ 不代表已经进入内核。

Binder 会在下一组文章中把 Java、JNI、Native libbinder 和驱动串起来,但 Binder 的跨进程能力不属于 JNI。

八、以后遇到 native 方法,先问这五个问题

看到 AOSP 中的 native xxx(),不要立刻全局搜索一个同名 C++ 函数。先按下面的顺序查:

text 复制代码
1. Java 声明是什么?
   类、方法名、static 与否、参数、返回值。

2. 绑定方式是什么?
   JNI 命名查找,还是 JNINativeMethod / RegisterNatives?

3. 注册入口在哪里?
   AndroidRuntime gRegJNI、模块 JNI_OnLoad,还是其他初始化路径?

4. C++ 入口参数分别是谁?
   JNIEnv、jobject/jclass、显式业务参数。

5. 进入 C++ 后又调用了谁?
   仍在用户态,还是最终进入系统调用、驱动或其他进程?

这五个问题能避免最常见的逻辑跳跃:从 Java 声明直接跳到 C++ 函数,却没有解释二者为什么能连上。

九、本文闭环:nativePollOnce() 到底去了哪里?

现在可以给出一个完整而不含糊的回答:

在典型 zygote 路径中,app_process 初始化阶段执行 register_android_os_MessageQueue(),把 Java 方法名、JNI 签名与 C++ 函数地址注册给 ART;后续 fork 出的进程继承这份映射。调用发生时,当前线程通过该映射进入 android_os_MessageQueue_nativePollOnce(),再继续执行 NativeMessageQueue::pollOnce()。第一篇到这里证明的是"方法如何连上";真正的对象关系与等待机制分别留给后两篇。

但调用代码里还有一个最显眼的问题没有解释:

cpp 复制代码
reinterpret_cast<NativeMessageQueue*>(ptr)

一个 Java long 为什么能变回 C++ 对象?Java MessageQueueNativeMessageQueue 是同一个对象吗?Java GC 能不能释放它?

下一篇沿着 nativeInit → mPtr → nativeDestroy 回答这些问题。

源码与延伸阅读

源码基准说明:本文以 Android 16 25Q4 / QPR2 源码快照为基准,具体以文中固定 commit hash 为准。该快照的 MessageQueue.java 存在 LegacyMessageQueueCombinedMessageQueueCombinedDeliMessageQueue 等实现变体。为了与前面的经典 Handler 队列语义连续,Java 片段以 LegacyMessageQueue/MessageQueue.java 为主;本文讨论的 JNI 声明和 Native 入口在这些变体间保持同一条核心链路。

相关推荐
hqk3 小时前
玩Android×AI Coding:从产品到代码,完整跑通一套 AI 软件研发工作流
android·ai编程·vibecoding
安卓修改大师4 小时前
安卓修改大师实战:深入解析Android主题Theme修改与沉浸式界面实现
android
stevenzqzq6 小时前
app性能日志工具
android
DB哥讲数据库6 小时前
【最新】MySQL9.7安装教程:图文详解(附MySQL安装包)
android·mysql·adb
IT乐手7 小时前
Android 写本地日志工具类
android·java
非典型Android程序员7 小时前
Launcher学习总结
android
Coffeeee8 小时前
Android功耗优化,哪些代码会让你的机子发热又耗电
android·架构·kotlin
xianrenli389 小时前
Android14自动时区更新
android·学习·framework·aosp14·自动时区
心中有国也有家9 小时前
AtomGit Flutter 鸿蒙客户端:零外部资源的应用打包策略
android·javascript·flutter·华为·harmonyos