JNI(三):Handler 没有消息时为什么会睡眠?新消息来了又是怎么被唤醒的?

39 阅读15分钟

JNI(三):Handler 没有消息时为什么会睡眠?新消息来了又是怎么被唤醒的?

前两篇分别解决了两个断点:

第一篇:Java native 方法怎样绑定到 C++ 函数?
第二篇:Java MessageQueue 怎样通过 mPtr 找到 NativeMessageQueue?

现在回到 Handler 最初的问题:

Java 消息队列没有可执行消息时,目标 Looper 线程为什么可以睡眠;发送线程插入更早消息后,它又为什么能及时醒来?

如果只说“因为 JNI”,答案并不准确。

JNI 只负责把调用带过 Managed / Native 边界。真正的等待和唤醒由 Native Looper、epolleventfd 与 Linux 内核完成;醒来以后,仍由 Java MessageQueue 决定哪条消息可以出队。

本文把整条链拆成三段:

Java 决策
计算 Java 消息允许等待的上限、是否需要 wake、醒来后取哪条消息

JNI 连接
把 Java 方法和参数送入 Native 实现

Native / Kernel 机制
用 epoll_wait 等事件,用 eventfd 发唤醒信号

只有把职责拆开,才不会把 JNI、线程阻塞、消息执行和 Binder 混成一团。

**范围说明:**本文主线基于 Android 16 LegacyMessageQueueCombinedMessageQueue 可按 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 值表达三种意图:

nextPollTimeoutMillisJava 侧含义Native 侧仍可能发生什么
0不允许因 Java Message 阻塞Native 仍可执行零超时 fd 检查、处理已到期 Native Message;满足条件时也可跳过这次 epoll_wait()
正数Java Message 最多允许等待指定毫秒Native Message 或 fd 仍可使它提前返回
-1Java 侧当前没有 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 自己也可以调度 MessageEnvelopemNextMessageUptime 记录其最早截止时间。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()
                                             ↓
                                      重新检查消息链表

image.png

七、nativeWake() 留下 wake 状态,但不执行消息

这是 Handler 与 JNI 交界处最常见的误解之一。

错误说法:

发送线程调用 nativeWake(),Native 层开始执行新消息。

准确说法:

nativeWake() 通过 eventfd 留下一份可观察的 wake 状态,使目标 Looper 不能继续按旧条件等待:如果它已经在 epoll_wait() 中,等待会返回;如果它尚未真正进入,下一次 epoll_wait() 会立即看到 fd 可读。随后线程回到 Java MessageQueue.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 随后的队列检查又会看到新状态。队列锁、mBlockedneedWake、非阻塞 poll 和 eventfd 可读状态共同覆盖了这些关键时序。

第四种时序说明,不能把 mBlocked 翻译成“线程此刻正睡在内核”。三个状态属于不同层次:

状态准确含义为什么不能互相替代
Java mBlockedMessageQueue 已建立本轮等待协议,后续入队可能需要使旧条件失效native poll 返回后不会立刻清零;next() 主循环真正取出到期消息时才改回 false
Native mPollingNative 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 无法到达。

image.png

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++ 的运行时跨越;BpBinderIPCThreadStateParcel 已进入 Binder 用户态实现,随后 Binder 驱动负责让事务跨越进程地址空间并唤醒目标线程。

下一篇就从这个问题继续:当调用目标位于 system_server 或另一个 Native Service 时,这次调用究竟怎样离开当前进程?

源码与延伸阅读

附录:本组三篇有意拆出的专题 `System.loadLibrary / linker / JNI_OnLoad`、完整 JNI 类型与引用表、Native 线程回调 Java与 ClassLoader、字符串和数组复制、CheckJNI 与 tombstone 排障,以及 ART JNI Stub / GC safepoint,分别属于库加载、引用生命周期、排障或 ART 内部实现专题,不是 Handler 等待—唤醒闭环的必要前置。