JNI(三):Handler 没有消息时为什么会睡眠?新消息来了又是怎么被唤醒的?
前两篇分别解决了两个断点:
第一篇:Java native 方法怎样绑定到 C++ 函数?
第二篇:Java MessageQueue 怎样通过 mPtr 找到 NativeMessageQueue?
现在回到 Handler 最初的问题:
Java 消息队列没有可执行消息时,目标 Looper 线程为什么可以睡眠;发送线程插入更早消息后,它又为什么能及时醒来?
如果只说“因为 JNI”,答案并不准确。
JNI 只负责把调用带过 Managed / Native 边界。真正的等待和唤醒由 Native Looper、epoll、eventfd 与 Linux 内核完成;醒来以后,仍由 Java MessageQueue 决定哪条消息可以出队。
本文把整条链拆成三段:
Java 决策
计算 Java 消息允许等待的上限、是否需要 wake、醒来后取哪条消息
JNI 连接
把 Java 方法和参数送入 Native 实现
Native / Kernel 机制
用 epoll_wait 等事件,用 eventfd 发唤醒信号
只有把职责拆开,才不会把 JNI、线程阻塞、消息执行和 Binder 混成一团。
**范围说明:**本文主线基于 Android 16
LegacyMessageQueue。CombinedMessageQueue可按 Framework 平台 flag 和进程条件启用 Concurrent 实现,其队列结构和跨线程mPtr保护不属于本文范围;具体版本判定见第二篇文末附注。
一、等待之前,Java 先计算 Java Message 的等待上限
MessageQueue.next() 不是一上来就永久睡眠,而是在循环里不断计算下一次等待策略。下面省略同步屏障、退出和 IdleHandler 分支,仅展示“没有可执行消息且没有 IdleHandler,需要继续等待”时的主路径:
final long ptr = mPtr;
int nextPollTimeoutMillis = 0;
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message msg = mMessages;
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis =
(int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
mBlocked = false;
return msg;
}
} else {
nextPollTimeoutMillis = -1;
}
// 没有 IdleHandler,需要进入下一轮等待
mBlocked = true;
continue;
}
}
这是为突出主线整理后的片段。存在 IdleHandler 时,源码会保持 mBlocked == false,离开锁执行回调,并在执行后把 nextPollTimeoutMillis 重置为 0;完整分支见 MessageQueue.next()。
三个 timeout 值表达三种意图:
nextPollTimeoutMillis | Java 侧含义 | Native 侧仍可能发生什么 |
|---|---|---|
0 | 不允许因 Java Message 阻塞 | Native 仍可执行零超时 fd 检查、处理已到期 Native Message;满足条件时也可跳过这次 epoll_wait() |
| 正数 | Java Message 最多允许等待指定毫秒 | Native Message 或 fd 仍可使它提前返回 |
-1 | Java 侧当前没有 Message 截止时间 | Native 仍可缩短 timeout,也可被普通 fd、wake eventfd 或信号提前唤醒 |
这里得到的是 Java 消息队列允许等待的上限,不一定是最终传给 epoll_wait() 的 timeout。Native Looper 还会结合自己的 Native Message 截止时间,取两边更早的时间。
所以必须避免一句常见但不准确的话:
调用
nativePollOnce(),线程就睡了。
准确说法是:
nativePollOnce()把 Java 侧计算的等待上限交给 Native Looper;Native 层完成自己的 timeout 合并后,只有真正执行到带非零等待条件的epoll_wait()时,线程才可能阻塞。
Android 16 的 Native Looper 甚至明确允许在满足条件时跳过零超时 epoll_wait(),进一步说明“进入 native 方法”和“进入内核等待”不是同一件事。
二、JNI 只负责连接,不负责真正等待
Java 调用:
nativePollOnce(ptr, nextPollTimeoutMillis);
经过已经注册好的 (JI)V 映射,进入:
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);
}
然后:
void NativeMessageQueue::pollOnce(
JNIEnv* env, jobject pollObj, int timeoutMillis) {
mPollEnv = env;
mPollObj = pollObj;
mLooper->pollOnce(timeoutMillis);
mPollObj = NULL;
mPollEnv = NULL;
}
到这里,JNI 已经完成它在等待路径上的主要任务:
定位 C++ 函数
传递当前线程的 JNIEnv
传递 Java MessageQueue 的 jobject
传递 NativeMessageQueue 句柄
传递 Java 侧计算的 timeout 上限
JNI 没有实现消息排序,没有决定哪条消息出队,也没有提供内核等待队列。
三、Looper 怎样准备 epoll 和唤醒通道?
Native Looper 构造时先创建一个 eventfd:
Looper::Looper(bool allowNonCallbacks) {
mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
rebuildEpollLocked();
}
重建 epoll 集合时,再把 wake fd 注册进去:
mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));
epoll_event wakeEvent = createEpollEvent(EPOLLIN, WAKE_EVENT_FD_SEQ);
epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD,
mWakeEventFd.get(), &wakeEvent);
源码位置:Looper 构造与 rebuildEpollLocked()。
可以把它理解成:
epoll 实例
├─ wake eventfd ← 专门用于主动叫醒 Looper
├─ input fd ← 例如输入等已注册事件
├─ display / sensor… ← 取决于调用者注册了什么
└─ 其他 fd
在本文追踪的 Handler 分支中,Java 队列状态变化会通过 nativeWake() 写这个 eventfd;但它本质上是 Native Looper 的通用唤醒通道。Native 层调度新的 Native Message、安排重建 epoll 集合等状态变化,也可能调用 Looper::wake()。因此,eventfd 可读本身不等于“来了一条 Java Message”。
Java MessageQueue 不只需要等待 Java 链表中的 Message 截止时间。Native Looper 还能把多个 fd 事件放进同一个等待点,这正是它不能简单替换成 Thread.sleep() 的原因。
Thread.sleep(1000) 表达“时间没到之前先暂停”;epoll_wait(..., 1000) 表达“最多等 1000 毫秒,但任一受关注 fd 就绪时可以提前返回”。
四、真正让线程等待的是 epoll_wait()
Native Looper 自己也可以调度 MessageEnvelope,mNextMessageUptime 记录其最早截止时间。Looper::pollInner() 会先合并两边 timeout,再进入核心等待点:
int originalTimeout = timeoutMillis;
// Native Looper 还可能根据自己的下一条 Native Message 缩短 timeout。
if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
int messageTimeoutMillis =
toMillisecondTimeoutDelay(now, mNextMessageUptime);
if (messageTimeoutMillis >= 0
&& (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
timeoutMillis = messageTimeoutMillis;
}
}
int eventCount = 0;
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
if (!sSkipEpollWaitIfPossible
|| originalTimeout != 0
|| mRequests.size() > 0) {
std::atomic_store_explicit(&mPolling, true,
std::memory_order_relaxed);
eventCount = epoll_wait(
mEpollFd.get(),
eventItems,
EPOLL_MAX_EVENTS,
timeoutMillis);
std::atomic_store_explicit(&mPolling, false,
std::memory_order_relaxed);
}
因此,真正进入内核前还有一次合并:
Java MessageQueue
计算 Java Message 允许等待的上限
↓
Native Looper
结合 mNextMessageUptime,取更早的 timeout
↓
epoll_wait(..., timeoutMillis)
originalTimeout 保存 Java 传入的原始值,供“零 timeout 是否可以跳过 epoll”判断;调整后的 timeoutMillis 才是本轮实际交给 epoll_wait() 的值。
源码位置:Looper::pollInner()。
现在三条边界可以精确标出来:
MessageQueue.nativePollOnce()
│
│ JNI 边界:Managed → Native
▼
NativeMessageQueue::pollOnce()
│
│ 用户态 C++ 普通调用
▼
Looper::pollInner()
│
│ system call 边界:User Space → Kernel
▼
epoll_wait()
如果没有事件且 timeout 尚未到期,内核会让当前 Linux 线程进入等待状态。这里仍然是发起 MessageQueue.next() 的那个 Looper 线程。
当 fd 就绪、timeout 到期或等待被信号中断时,epoll_wait() 返回,但不一定立刻回到 Java。Native Looper 还会处理到期的 Native MessageEnvelope 和 fd callback;普通 fd 就绪时,NativeMessageQueue::handleEvent() 还可能在同一次 nativePollOnce() 内回调 Java dispatchEvents()。这些工作完成后,调用才从 nativePollOnce() 返回 Java MessageQueue.next()。
五、另一个线程插入消息后,为什么可能需要 nativeWake()?
假设目标 Looper 线程当前按 10 秒 timeout 等待,发送线程插入一条“现在就应该执行”的消息。
只修改 Java 链表还不够:内核中等待的目标 Looper 线程并不知道 Java 侧出现了更早的截止时间,可能继续等满原来的 10 秒。
因此,enqueueMessage() 在持有 MessageQueue 锁时判断新消息是否改变当前等待结果:
下面是保留 needWake 判断关系后的逻辑骨架,队列遍历和重复异步消息判断已省略:
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// 新消息成为队头
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// 同步屏障场景下,最早的异步消息也可能需要 wake
needWake = mBlocked
&& p.target == null
&& msg.isAsynchronous();
// 插入队列,并继续缩小 needWake 条件
}
if (needWake) {
nativeWake(mPtr);
}
完整源码见 MessageQueue.enqueueMessage()。
needWake 不是“只要入队就为 true”。它主要回答:
MessageQueue已经建立了本轮旧等待条件,而这条新消息是否让该条件失效?
例如把一条更晚的消息插到队尾,通常不必唤醒;目标 Looper 线程按原来的更早截止时间醒来即可。
六、nativeWake() 怎样让 epoll_wait() 返回?
Java:
nativeWake(mPtr);
JNI:
static void android_os_MessageQueue_nativeWake(
JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue =
reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}
NativeMessageQueue:
void NativeMessageQueue::wake() {
mLooper->wake();
}
Native Looper:
下面只保留 eventfd 的核心读写动作,省略源码中的 EINTR 重试、返回值检查与 EAGAIN 处理;后面的 awoken() 片段也做同样简化。
void Looper::wake() {
uint64_t inc = 1;
write(mWakeEventFd.get(), &inc, sizeof(uint64_t));
}
源码位置:NativeMessageQueue::wake()、Looper::wake()。
eventfd 内部维护一个计数器。写入 1 会增加计数,使这个 fd 变为或保持可读;它已经被注册到 epoll 集合。若目标线程正在 epoll_wait(),等待会返回 wake 事件;若它尚未真正进入等待,下一次 epoll_wait() 也会立即观察到这个可读状态。
Native Looper 处理 wake 事件时读走计数:
void Looper::awoken() {
uint64_t counter;
read(mWakeEventFd.get(), &counter, sizeof(uint64_t));
}
这里创建 eventfd 时没有使用 EFD_SEMAPHORE:多次写入会累加计数,一次读取会取得累积值并把计数清零。这里的“合并”有明确批次边界:同一次 awoken() 读取前发生的写入共享一个累计计数;读取清零以后再发生的新写入属于下一批,会让 eventfd 再次变为可读。awoken() 不会按计数执行同样数量的 Message,它只是消费当前批次的唤醒状态,让 Looper 回到 Java 队列重新判断。
下面只追踪 wake eventfd 分支。其他 fd 就绪时,Native Looper 可能先执行 Native callback 或回调 Java dispatchEvents(),再从 nativePollOnce() 返回。
这条 wake 分支可以收束为:
发送线程 Looper 线程
Handler.sendMessage()
↓
MessageQueue.enqueueMessage()
↓ needWake == true
nativeWake(mPtr)
↓ JNI
NativeMessageQueue::wake()
↓
Looper::wake()
↓
write(eventfd, 1) ────────────────▶ epoll_wait() 返回
↓
Looper::awoken()
↓
返回 Java next()
↓
重新检查消息链表
七、nativeWake() 留下 wake 状态,但不执行消息
这是 Handler 与 JNI 交界处最常见的误解之一。
错误说法:
发送线程调用
nativeWake(),Native 层开始执行新消息。
准确说法:
nativeWake()通过 eventfd 留下一份可观察的 wake 状态,使目标 Looper 不能继续按旧条件等待:如果它已经在epoll_wait()中,等待会返回;如果它尚未真正进入,下一次epoll_wait()会立即看到 fd 可读。随后线程回到 JavaMessageQueue.next(),重新按when、同步屏障、异步标记和退出状态检查队列。真正取出消息后,才由Looper.loopOnce()走向msg.target.dispatchMessage(msg)。
所以职责是:
nativeWake
负责“旧等待条件已经失效,请重新检查状态”
MessageQueue.next
负责“现在到底哪条消息可以出队”
Handler.dispatchMessage
负责“这条消息具体执行什么”
把这三步说成“唤醒并执行消息”,会掩盖 Java 队列仍然拥有最终决策权。
八、mBlocked 与 eventfd 怎样覆盖等待边界上的关键时序?
这一节用于解释为什么不会丢失 wake。第一次阅读只需掌握前三种主时序;第四种用于理解为什么可能出现冗余 wake。
完整协议不只要解释 write 发生在 epoll_wait() 前后,还要解释 mBlocked == false 时为什么可以不写,以及 epoll_wait() 已返回但 Java 还没重新检查队列的窗口。关键是 MessageQueue.next() 设置 mBlocked,与 enqueueMessage() 判断队列和 needWake,都使用同一把队列锁:
1. 入队发生在 Looper 设置 mBlocked = true 之前,或目标线程正在 dispatch / IdleHandler 阶段
→ mBlocked == false 表示 Looper 尚未承诺按旧条件阻塞。
→ 发送线程可以不 wake;dispatch 结束后新一轮 next() 的 timeout 初值为 0,
IdleHandler 返回后当前 next() 也会把 timeout 重置为 0,再非阻塞 poll 并检查队列。
2. 入队发生在 mBlocked = true 之后、epoll_wait() 之前
→ 发送线程在同一把锁下看到 mBlocked == true。
→ write(eventfd) 后 fd 已可读;随后 epoll_wait() 会立即观察到。
3. 入队发生在 epoll_wait() 之后
→ write(eventfd) 增加计数,使 fd 变为或保持可读。
→ 正在等待的目标 Looper 线程因该就绪事件返回。
4. 入队发生在 epoll_wait() 已返回、Java 尚未重新检查队列之间
→ Native Looper 已把 mPolling 设回 false,但 Java 的 mBlocked 可能仍为 true。
→ 发送线程可能执行一次已经不再必需的 nativeWake。
→ 这个窗口还可能覆盖到期 Native Message、fd callback 和 dispatchEvents() 回调阶段。
→ eventfd 持续保存 wake 状态,直到后续某次真正执行 epoll_wait() 的 poll
观察到它,再由 awoken() 读走;跳过 epoll 的零 timeout 轮询不会消费它。
→ 多个线程可能产生一次或多次冗余 write()。同一次 awoken() 读取前的写入
会合并成一个非零计数和一次可读状态;读取后的新写入属于下一批,
仍可能让后续 epoll_wait() 再次立即返回。eventfd 不保证任意数量的 wake
永远只产生一次返回,但这套协议不会漏掉消息。
next() 每次重新进入时把 nextPollTimeoutMillis 初始化为 0,执行完 IdleHandler 后也会显式重置为 0;所以这两个阶段入队却没有 wake,并不会让线程直接带着旧 timeout 睡下去。再加上同一把队列锁的约束:Looper 持锁时,发送线程不能在“检查队列”和“设置 mBlocked”之间插入修改;发送线程先完成入队时,Looper 随后的队列检查又会看到新状态。队列锁、mBlocked、needWake、非阻塞 poll 和 eventfd 可读状态共同覆盖了这些关键时序。
第四种时序说明,不能把 mBlocked 翻译成“线程此刻正睡在内核”。三个状态属于不同层次:
| 状态 | 准确含义 | 为什么不能互相替代 |
|---|---|---|
Java mBlocked | MessageQueue 已建立本轮等待协议,后续入队可能需要使旧条件失效 | native poll 返回后不会立刻清零;next() 主循环真正取出到期消息时才改回 false |
Native mPolling | Native Looper 正处于 epoll_wait() 调用周边的 polling 阶段 | 它在调用前设为 true,返回后立即设为 false,与 Java 锁和 mBlocked 不同步 |
| Linux 线程实际状态 | 线程此刻由内核调度为运行、可运行或等待等状态 | 最终由系统调用与调度器决定,不由某个 Java/C++ 布尔值精确镜像 |
mPolling 使用 memory_order_relaxed 原子读写,只能提供一个天然带竞态的状态快照,适合观察和排障,不能作为其他内存写的同步屏障。观察到 true 也不能据此断言目标线程已经睡眠、状态不会马上变化,更不能绕过 MessageQueue 锁或 mBlocked + needWake + eventfd 协议。
因此,mBlocked == true 允许一次保守但冗余的 wake;这个协议追求的是“不漏掉使旧等待失效的队列变化”,而不是让 Java 状态逐指令同步内核睡眠状态。
还要把 wake 与 Message 明确拆开:
Message:待执行的工作,保存在 Java MessageQueue 中。
needWake:新队列状态是否使旧等待条件失效。
eventfd:通知 Looper 回来重新检查队列。
一次 wake 不代表一条 Message,也不要求二者一一对应。多次队列变化可以被一次重新检查吸收,一次 wake 后也可能发现尚无可出队消息;Java MessageQueue 才是消息状态的事实来源。
排障提示:线程栈停在
nativePollOnce / epoll_wait通常只说明 Looper 正在等待,不能单凭这一帧判断卡死。具体诊断见 Handler 卡顿怎么定位?从 Slow delivery 到 Slow dispatch。
九、职责边界:谁负责什么?
把调用链按责任分层:
| 机制 | 负责 | 不负责 |
|---|---|---|
Java MessageQueue | 计算下一条 Java Message 的截止时间、屏障、needWake、出队 | C++ 方法绑定、Native Message 调度 |
| JNI | 方法映射、参数和引用按规则跨 Managed/Native 边界 | 消息调度、线程池、IPC |
Native Looper | 合并 Java timeout、Native Message 截止时间和 fd 事件,管理 wake fd 与 mPolling 并调用 epoll | 决定 Java 哪条 Message 出队 |
epoll / eventfd | 内核事件等待与主动唤醒信号 | Java 对象生命周期 |
| Linux scheduler | 线程的可运行与等待调度 | Handler 业务语义 |
JNI 解决“调用怎样过边界”,Handler 解决“消息怎样调度”,epoll/eventfd 解决“线程怎样等待和被叫醒”。
十、JNI 边界在哪里结束,Binder 调用链怎样继续?
到这里,Handler 与 JNI 的调用仍然停留在同一进程、同一地址空间。若目标对象位于另一个进程,仅靠 JNI 无法到达。
Android Java Binder 恰好会把两者串起来:
Java BinderProxy.transact()
↓
transactNative()
↓ JNI:离开 Managed Code,但仍在客户端进程
android_os_BinderProxy_transact()
↓
BpBinder::transact()
↓
IPCThreadState::transact()
↓
ioctl(BINDER_WRITE_READ)
↓ Binder driver:开始跨进程通信
远端 Binder 线程
边界可以压缩成一句话:JNI 在客户端进程内完成 Java → C++ 的运行时跨越;BpBinder、IPCThreadState 与 Parcel 已进入 Binder 用户态实现,随后 Binder 驱动负责让事务跨越进程地址空间并唤醒目标线程。
下一篇就从这个问题继续:当调用目标位于 system_server 或另一个 Native Service 时,这次调用究竟怎样离开当前进程?
源码与延伸阅读
- Android 16 25Q4 / QPR2
LegacyMessageQueue/MessageQueue.java - Android 16 25Q4 / QPR2
android_os_MessageQueue.cpp - Android 16 25Q4 / QPR2
libutils/Looper.cpp - Linux
eventfd(2)manual - Linux
epoll_wait(2)manual - Android Developers:JNI tips
- 下一篇:Binder(一)一次方法调用,究竟怎样跨进程执行?