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*、jobject、jlong ptr分别是谁?- JNI、NDK、Binder 为什么经常一起出现,却不是一回事?
这三篇不按"类型表---API 表---引用表"的传统顺序讲 JNI,而是沿着 Handler 留下的问题继续向下追。
本文先只解决一件事:
当前线程执行
MessageQueue.nativePollOnce()时,怎样通过 JNI 找到 C++ 实现,并继续在同一个线程中运行?
下一篇再回答 mPtr 怎样关联 Java MessageQueue 与 NativeMessageQueue,第三篇完成等待、唤醒闭环,并说明 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 Thread 和 Native 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);
同一版本还注册了 nativeIsPolling、nativeSetFileDescriptorEvents 和 nativeSetSkipEpollWaitForZeroTimeout;它们不属于本文这条四方法主线。
源码位置: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 long,I 表示 Java int,V 表示 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.cpp 的 gRegJNI:
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 MessageQueue 和 NativeMessageQueue 是同一个对象吗?Java GC 能不能释放它?
下一篇沿着 nativeInit → mPtr → nativeDestroy 回答这些问题。
源码与延伸阅读
- Android 16 25Q4 / QPR2
LegacyMessageQueue/MessageQueue.java - Android 16 25Q4 / QPR2
android_os_MessageQueue.cpp - Android 16 25Q4 / QPR2
AndroidRuntime.cpp - Android 16 25Q4 / QPR2
app_process - Android 16 25Q4 / QPR2
ZygoteInit.java - Android Developers:JNI tips
- Oracle Java 21:JNI Design Overview
- 下一篇:JNI(二):Java 里的一个 long,为什么能找到 C++ 对象?
源码基准说明:本文以 Android 16 25Q4 / QPR2 源码快照为基准,具体以文中固定 commit hash 为准。该快照的
MessageQueue.java存在LegacyMessageQueue、CombinedMessageQueue、CombinedDeliMessageQueue等实现变体。为了与前面的经典 Handler 队列语义连续,Java 片段以LegacyMessageQueue/MessageQueue.java为主;本文讨论的 JNI 声明和 Native 入口在这些变体间保持同一条核心链路。