Android-Framework(十一)-彻底理解Handler的通信过程

文章内容

一、Handler 的循环为何不烧满 CPU

Looper.loop() 中确实存在一个没有退出次数限制的 for (;;)。但“循环一直存在”和“CPU 一直执行循环体”是两回事。只要当前没有可立即处理的消息,Java 层就会把等待时间交给 nativePollOnce,native 层最终进入 epoll_wait。线程此时处于阻塞等待状态,不会反复读取队列。

这条等待链可以总结为3点:

  1. 延迟消息把“距离执行还剩多久”换算为 nextPollTimeoutMillis,由 epoll_wait 定时返回;如果更早的消息中途入队,nativeWake 会提前唤醒等待并重算超时。
  2. 队列没有立即消息可取时,MessageQueue.next() 会先给 IdleHandler 一次执行机会,再决定是否继续阻塞。
  3. 一条消息从 MessageQueue 返回后,Looper 在 dispatchMessage 前后调用 Printer,两次日志之间的时间就是这次派发占用主线程的时间。

Handler 消息循环在同一套等待机制上支撑延迟消息、空闲回调与主线程耗时监听三件事

二、全局概览:Java 队列如何落到 native epoll

1. 架构设计

Java 层参与消息通信的主要对象是 Handler、Looper、MessageQueue 和 Message:

  • Handler 负责发送消息,并作为 Message.target 接收派发。
  • MessageQueue 用按 when 排序的单链表保存 Java Message。
  • Looper 驱动 MessageQueue.next(),拿到消息后调用 msg.target.dispatchMessage(msg)。
  • Message 携带执行时间、目标 Handler、回调或业务数据。

native 层可见 NativeMessageQueue 和 Looper。它们承担阻塞、文件描述符监听与唤醒,并没有再保存一份 Java Message 链表。Java 与 C++ 也不存在 Handler ↔ Handler、Looper ↔ Looper 的完整一一映射。真正跨过 JNI 边界的是 Java MessageQueue.mPtr:它保存 NativeMessageQueue 的地址,后续的 poll、wake 和销毁都靠这个值找到 native 对象。

Java 与 C++ 两层对象的分层结构:仅 MessageQueue 靠 mPtr / nativeInit 跨层连通,其余并不一一对应

2. mPtr 跨层连接

先看 native 方法声明,划出 Java MessageQueue 与 native 世界的接口边界。

java
// 代码位置 /frameworks/base/core/java/android/os/MessageQueue.java 第 61—66 行// 创建 native 队列并返回其地址。private native static long nativeInit();// 释放 ptr 指向的 native 队列。private native static void nativeDestroy(long ptr);// 按给定超时时间等待;它是实例方法。private native void nativePollOnce(long ptr, int timeoutMillis);// 唤醒正在等待的 native Looper。private native static void nativeWake(long ptr);// 查询 native 队列当前是否处于轮询状态。private native static boolean nativeIsPolling(long ptr);// 更新指定文件描述符关心的事件。private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);

nativeInit() 的输出是一个 long,Java 侧把它保存为 mPtr;其余方法再把 mPtr 作为输入。这里发生的状态变化不是“把 Java 消息复制到 C++”,而是让同一个 Java 队列获得一个 native 等待器。

JNI 实现进一步证明了指针的去向。看这段代码的目的,是确认 mPtr 指向的对象类型,以及 poll 与 wake 最终分别调用谁。

cpp
// 代码位置 /frameworks/base/core/jni/android_os_MessageQueue.cpp 第 172—181 行// 定义创建 native 队列的 JNI 函数。static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {    // 在 native 堆上创建 NativeMessageQueue。    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();    // 增加强引用,生命周期由 JNI 桥接层管理。    nativeMessageQueue->incStrong(env);    // 把对象地址转换为 Java long 返回。    return reinterpret_cast<jlong>(nativeMessageQueue);// 结束初始化函数。}// 代码位置 /frameworks/base/core/jni/android_os_MessageQueue.cpp 第 188—192 行// 定义单次轮询的 JNI 函数。static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj, jlong ptr, jint timeoutMillis) {    // 把 Java 传入的 long 还原为 native 队列指针。    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);    // 将超时时间交给 NativeMessageQueue。    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);// 结束轮询函数。}// 代码位置 /frameworks/base/core/jni/android_os_MessageQueue.cpp 第 194—197 行// 定义主动唤醒的 JNI 函数。static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {    // 还原 NativeMessageQueue 指针。    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);    // 调用 native 队列的唤醒入口。    nativeMessageQueue->wake();// 结束唤醒函数。}// 代码位置 /frameworks/base/core/jni/android_os_MessageQueue.cpp 第 212—221 行// 声明 Java 方法与 native 函数的映射表。static JNINativeMethod gMessageQueueMethods[] = {    // 注册 nativeInit 的签名和实现地址。    {"nativeInit", "()J", (void*)android_os_MessageQueue_nativeInit},    // 注册 nativeDestroy 的签名和实现地址。    {"nativeDestroy", "(J)V", (void*)android_os_MessageQueue_nativeDestroy},    // 注册 nativePollOnce 的签名和实现地址。    {"nativePollOnce", "(JI)V", (void*)android_os_MessageQueue_nativePollOnce},    // 注册 nativeWake 的签名和实现地址。    {"nativeWake", "(J)V", (void*)android_os_MessageQueue_nativeWake},    // 注册 nativeIsPolling 的签名和实现地址。    {"nativeIsPolling", "(J)Z", (void*)android_os_MessageQueue_nativeIsPolling},    // 注册文件描述符事件更新方法。    {"nativeSetFileDescriptorEvents", "(JII)V", (void*)android_os_MessageQueue_nativeSetFileDescriptorEvents},// 结束 JNI 方法表。};// 目前能确认的注册入口:register_android_os_MessageQueue 用 RegisterMethodsOrDie 挂到 Java 类。// 代码位置 /frameworks/base/core/jni/android_os_MessageQueue.cpp 第 223—233 行// 定义注册入口函数。int register_android_os_MessageQueue(JNIEnv* env) {    // 把 gMessageQueueMethods 注册到 android/os/MessageQueue 类。    int res = RegisterMethodsOrDie(env, "android/os/MessageQueue", gMessageQueueMethods, NELEM(gMessageQueueMethods));    // 取得 MessageQueue 类对象。    jclass clazz = FindClassOrDie(env, "android/os/MessageQueue");    // 取出 MessageQueue 的 mPtr 字段 ID,供后续读取 native 指针。    gMessageQueueClassInfo.mPtr = GetFieldIDOrDie(env, clazz, "mPtr", "J");    // 返回注册结果。    return res;// 结束注册入口函数。}// 这个函数自身被更高层 JNI 注册表调用;该调用点的符号名在素材分支中待核实,不可杜撰。// NativeMessageQueue::pollOnce 把 timeout 转发给 Looper:Java 的 nativePollOnce 最终落到 epoll。// 代码位置 /frameworks/base/core/jni/android_os_MessageQueue.cpp 第 107—119 行// 定义 NativeMessageQueue 的 pollOnce 转发。void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {    // 记录 JNI 环境与 Java 对象引用,供唤醒后回调使用。    mPollEnv = env;    // 记录本次轮询对应的 Java MessageQueue 对象。    mPollObj = pollObj;    // 把等待真正交给 native Looper。    mLooper->pollOnce(timeoutMillis);    // 调用结束后清理 Java 对象引用。    mPollObj = NULL;    // 调用结束后清理 JNI 环境引用。    mPollEnv = NULL;    // 若等待期间有异常发生,转交给 Java 层抛出。    if (mExceptionObj) {        // 抛出之前记录的异常。        env->Throw(mExceptionObj);        // 释放这个异常的本地引用。        env->DeleteLocalRef(mExceptionObj);        // 清空异常指针,避免重复抛出。        mExceptionObj = NULL;    }// 结束 NativeMessageQueue::pollOnce。}

输入 ptr 后,JNI 先恢复 NativeMessageQueue*,输出则表现为等待结束或唤醒生效。调用链由此从 Java 的 MessageQueue 收敛到 native 的 NativeMessageQueue,这也是分层图里最关键的一条连线。

三、发送主链:sendMessage 最终只是有序入队

Handler.sendMessage() 会逐级转到带执行时刻的发送接口,最后进入 MessageQueue.enqueueMessage(msg, when)。队列按 when 插入消息,when == 0 或更早到期的消息会成为新队首。

看 enqueueMessage,重点不是链表插入写法,而是插入过程中如何得出 needWake。

java
// 代码位置 /frameworks/base/core/java/android/os/MessageQueue.java 第 533—585 行// 定义消息入队方法。private boolean enqueueMessage(Message msg, long when) {    // 拒绝没有目标 Handler 的普通消息。    if (msg.target == null) throw new IllegalArgumentException("Message must have a target.");    // 拒绝重复使用正在流转的 Message。    if (msg.isInUse()) throw new IllegalStateException(msg + " This message is already in use.");    // 锁住队列,保证链表操作的原子性。    synchronized (this) {        // 退出中的队列不再接收消息。        if (mQuitting) { msg.recycle(); return false; }        // 标记消息已经进入使用状态。        msg.markInUse();        // 保存预定执行时刻。        msg.when = when;        // 取得当前队首。        Message p = mMessages;        // 声明是否需要唤醒阻塞线程。        boolean needWake;        // 判断消息是否应成为新队首。        if (p == null || when == 0 || when < p.when) {            // 把原队首接到新消息之后。            msg.next = p;            // 更新队首为新消息。            mMessages = msg;            // 只有线程确实阻塞时才需要唤醒。            needWake = mBlocked;        // 处理插入队列中间或尾部的情况。        } else {            // 屏障位于队首且新消息为异步消息时,可能需要唤醒。            needWake = mBlocked && p.target == null && msg.isAsynchronous();            // 声明前驱节点。            Message prev;            // 沿有序链表寻找插入点。            for (;;) {                // 同步推进前驱和当前节点。                prev = p; p = p.next;                // 到达链表末尾或遇到更晚到期的消息时停止。                if (p == null || when < p.when) break;                // 若当前已有更早的异步消息,就不再重复唤醒(屏障只唤醒一次)。                if (needWake && p.isAsynchronous()) needWake = false;            // 结束插入点搜索。            }            // 新消息连接后继。            msg.next = p; prev.next = msg;        // 结束插入位置分支。        }        // 队列未退出且等待条件已经改变时唤醒 native 层。        if (needWake && !mQuitting) { nativeWake(mPtr); }    // 释放队列锁。    }    // 告知调用方入队成功。    return true;// 结束入队方法。}

输入是带 target 的 Message 和绝对执行时刻 when;输出是是否入队成功。状态变化包括 msg.when 被写入、链表连接关系改变,以及必要时触发 nativeWake。这说明“发送消息”本身不执行业务,也不为每条延迟消息新建计时线程;它修改的是队列的下一次可执行时间。

四、消费主链:next() 决定等待多久

Looper.loop() 每轮调用 queue.next()。next() 的职责不只是出队,还要回答两个问题:当前是否存在可以越过屏障的消息,以及它离执行时刻还有多久。

看 next() 的主干,是为了找到 nextPollTimeoutMillis 的来源,并确认同步屏障在何处改变选中的消息。

java
// 代码位置 /frameworks/base/core/java/android/os/MessageQueue.java 第 307—411 行// 定义阻塞式取下一条消息的方法。Message next() {    // 复制 native 指针,避免循环中反复读取字段。    final long ptr = mPtr;    // native 对象已经销毁时结束取消息。    if (ptr == 0) { return null; }    // 用 -1 表示本轮尚未计算 IdleHandler 数量。    int pendingIdleHandlerCount = -1;    // 首轮先以零超时立即检查。    int nextPollTimeoutMillis = 0;    // 持续等待,直到取得消息或队列退出。    for (;;) {        // 进入非零时长等待前刷新 Binder 待处理命令。        if (nextPollTimeoutMillis != 0) { Binder.flushPendingCommands(); }        // 交给 native 层阻塞、定时返回或等待唤醒。        nativePollOnce(ptr, nextPollTimeoutMillis);        // 锁住 Java 消息链表进行检查。        synchronized (this) {            // 读取单调递增的系统运行时间。            final long now = SystemClock.uptimeMillis();            // 初始化候选消息的前驱。            Message prevMsg = null;            // 从队首开始查找。            Message msg = mMessages;            // 队首 target 为空表示遇到同步屏障。            if (msg != null && msg.target == null) {                // 越过屏障及后续同步消息。                do { prevMsg = msg; msg = msg.next; }                // 找到第一条异步消息或到达链表末尾。                while (msg != null && !msg.isAsynchronous());            // 结束屏障分支。            }            // 存在可以考虑执行的消息。            if (msg != null) {                // 消息尚未到达执行时刻。                if (now < msg.when) {                    // 计算剩余等待毫秒数并防止 int 溢出。                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);                // 消息已经到期,从链表中摘下并返回。                } else {                    // 标记队列不再阻塞。                    mBlocked = false;                    // 把前驱接到该消息的下一条。                    if (prevMsg != null) { prevMsg.next = msg.next; }                    // 否则直接成为新队首的跳过头。                    else { mMessages = msg.next; }                    // 断开取出的消息与链表的连接。                    msg.next = null;                    // 防止消息在派发期间被复用。                    msg.markInUse();                    // 返回这条可立即执行的消息。                    return msg;                // 结束到期判断。                }            // 没有候选消息:永久等待直到被唤醒。            } else {                // 用 -1 表示可以无限期等待。                nextPollTimeoutMillis = -1;            // 结束候选消息判断。            }            // 队列已经进入退出流程时释放 native 资源并结束。            if (mQuitting) {                // 销毁 native 队列。                dispose();                // 结束取消息。                return null;            // 结束退出处理。            }            // 首次迭代且当前无立即消息时统计空闲回调。            if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {                // 记录本轮准备执行的空闲回调数量。                pendingIdleHandlerCount = mIdleHandlers.size();            // 结束空闲统计。            }            // 没有空闲回调或尚未到达空闲状态时继续阻塞。            if (pendingIdleHandlerCount <= 0) {                // 标记队列已进入阻塞等待。                mBlocked = true;                // 进入下一轮,等待被唤醒。                continue;            // 结束无回调分支。            }            // 尚未创建临时数组时分配足够空间。            if (mPendingIdleHandlers == null) {                // 数组容量至少为四。                mPendingIdleHandlers = new IdleHandler[Math.max(mIdleHandlers.size(), 4)];            // 结束数组初始化。            }            // 制作快照,避免执行回调时直接遍历可变集合。            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);        // 释放消息队列锁。        }         // 仅在首次迭代、且上面确实进入空闲分支时执行 IdleHandler。        for (int i = 0; i < pendingIdleHandlerCount; i++) {            // 取得当前空闲回调。            final IdleHandler idler = mPendingIdleHandlers[i];            // 清除临时数组中的引用。            mPendingIdleHandlers[i] = null;            // 默认认为该回调不再保留。            boolean keep = false;            // 捕获回调自身抛出的异常。            try { keep = idler.queueIdle(); }            // 将回调异常记录为严重日志。            catch (Throwable t) { Log.wtf(TAG, "IdleHandler threw exception", t); }            // 返回 false 时从长期集合移除。            if (!keep) synchronized (this) { mIdleHandlers.remove(idler); }        // 结束本轮空闲回调遍历。        }        // 执行空闲回调期间可能又插入了新消息,因此清零重新检查。        pendingIdleHandlerCount = 0;        // 下一轮不再等待,立即再检查一次队列。        nextPollTimeoutMillis = 0;    // 继续下一轮取消息。    }// 结束 next 方法。}

nextPollTimeoutMillis 有三类关键值:

  • 0:不阻塞,立即检查 native 事件和 Java 消息。
  • 正数:最多等待指定毫秒数,适用于队首消息尚未到期。
  • -1:没有可执行时间点,允许无限期等待,直到文件描述符事件或 nativeWake 到来。

nativePollOnce 返回并不等于一定有 Java 消息。它可能是超时、wake fd 可读或其他注册 fd 就绪。控制流回到 Java 后仍要重新读取时间和队列,因为等待期间队列状态可能已经变化。

五、延迟消息:timeout、epoll 与 wake 的协作

1. epoll_wait 承担真正的休眠

假设队首消息将在 2 秒后执行,next() 得到的不是“循环检查 2 秒”,而是约 2000ms 的 timeout。JNI 继续调用 NativeMessageQueue.pollOnce,再进入 native Looper.pollOnce() 与 pollInner()。

看 native Looper 的目的,是确认 timeout 最终确实传给内核等待接口,并找到主动唤醒的写端。

cpp
// 代码位置 /system/core/libutils/Looper.cpp 第 184—218 行// 定义对外的单次轮询入口。int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {    // 初始化轮询结果。    int result = 0;    // 循环处理响应或进入等待。    for (;;) {        // 优先消费已经收集的响应。        while (mResponseIndex < mResponses.size()) {            // 取得当前响应并推进索引。            const Response& response = mResponses.itemAt(mResponseIndex++);            // 读取请求标识。            int ident = response.request.ident;            // 非负标识代表可直接返回的事件。            if (ident >= 0) { ...; return ident; }        // 结束已就绪响应处理。        }        // 已有唤醒、超时或错误结果时返回。        if (result != 0) { ...; return result; }        // 进入真正执行 epoll 的内部轮询。        result = pollInner(timeoutMillis);    // 继续检查内部轮询结果。    }// 结束 pollOnce。} // 代码位置 /system/core/libutils/Looper.cpp 第 220—376 行// 定义内部轮询实现。int Looper::pollInner(int timeoutMillis) {    // 存在 native 消息时也要比较它的到期时刻。    if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {        // 取得单调时钟当前值。        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);        // 换算 native 下一条消息的剩余毫秒数。        int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);        // native 消息更早到期时缩短等待时间。        if (messageTimeoutMillis >= 0 && (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {            // 采用更早的到期时长。            timeoutMillis = messageTimeoutMillis;        // 结束超时时间调整。        }    // 结束 native 消息时间检查。    }    // 默认结果设为被唤醒。    int result = POLL_WAKE;    // 清空上一轮响应。    mResponses.clear();    // 重置响应读取位置。    mResponseIndex = 0;    // 标记即将进入轮询。    mPolling = true;    // 准备接收 epoll 就绪事件的数组。    struct epoll_event eventItems[EPOLL_MAX_EVENTS];    // 让内核等待 fd 就绪或 timeout 到期。    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);    // 标记本轮内核等待已经结束。    mPolling = false;    // 加锁处理 epoll 状态。    mLock.lock();    // 必要时重建 epoll 注册集合。    if (mEpollRebuildRequired) { rebuildEpollLocked(); }    // 遍历 epoll_wait 返回的就绪事件,区分唤醒与普通 fd。    for (int i = 0; i < eventCount; i++) {        // 取出该事件注册时保存的 fd。        int fd = eventItems[i].data.fd;        // 取出该事件发生的事件掩码。        uint32_t epollEvents = eventItems[i].events;        // 等于 wake fd 且可读,说明是被主动唤醒。        if (fd == mWakeEventFd) {            // 读走唤醒计数,消费掉这次唤醒信号。            if (epollEvents & EPOLLIN) { awoken(); }        // 其他已注册 fd 进入各自的回调处理。        } else {            // 其他句柄回调;本文不展开。            ...        // 结束事件分发。        }    // 结束就绪事件遍历。    }// 结束 pollInner。}// 代码位置 /system/core/libutils/Looper.cpp 第 404—416 行// 定义主动唤醒方法。void Looper::wake() {    // eventfd 每次写入使用一个 64 位计数值。    uint64_t inc = 1;    // 写 wake fd,使 epoll 监听到可读事件;若系统调用被打断则重试。    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));    // 写入本身失败且不是缓冲区不可用时才告警。    if (nWrite != sizeof(uint64_t)) {        // 缓冲区已被占满(非缓冲区不可用)时忽略,其它错误记录日志。        if (errno != EAGAIN) { ALOGW("Could not write wake signal, errno=%d", errno); }    }// 结束 wake 方法。} // 代码位置 /system/core/libutils/Looper.cpp 第 152—158 行// epoll 注册时把 wake fd 本身存入 eventItem.data,不依赖额外的 ident 常量。// 定义 wake fd 注册逻辑(构造 Looper 时执行)。struct epoll_event eventItem;// 把事件结构体清零。memset(&eventItem, 0, sizeof(epoll_event));// 只监听可读事件。eventItem.events = EPOLLIN;// 把 wake fd 存进 epoll 事件数据,用于后续事件识别。eventItem.data.fd = mWakeEventFd;// 把 wake fd 注册进 epoll 集合。epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, &eventItem); // 代码位置 /system/core/libutils/Looper.cpp 第 290—296 行// epoll_wait 返回后就绪事件按 fd 判断;等于 wake fd 且可读,说明被唤醒。if (fd == mWakeEventFd) {    // 可读时调用 awoken 消费唤醒信号。    if (epollEvents & EPOLLIN) { awoken(); }    // 出现意外事件时记录日志。    else { ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents); }} // 代码位置 /system/core/libutils/Looper.cpp 第 418—425 行// 读走 eventfd 里的计数,消费掉这次唤醒信号。void Looper::awoken() {    // 用一个 uint64_t 接收,若系统调用被打断则重试。    uint64_t counter;    // 从 wake fd 读出计数,让读端复位。    TEMP_FAILURE_RETRY(read(mWakeEventFd, &counter, sizeof(uint64_t)));}// 说明:AOSP 没有为唤醒事件引入单独的 ident 常量;它直接在 epoll_event.data 存 fd,// 事件发生时用 “fd == mWakeEventFd” 判定是否为唤醒。对应的待核实项就此确认。

输入 timeoutMillis 后,pollInner 还可能依据 native 消息的到期时间缩短它,最终把结果交给 epoll_wait。输出可能是 fd 事件数量、超时、唤醒或错误状态。mPolling 在调用前后从 true 变回 false;wake() 则向 mWakeEventFd 写入 uint64_t,让已注册该 fd 的 epoll 立即返回。

epoll 监听 wake fd 的读端:写端写入一个数,读端可读后 epoll_wait 返回并唤醒

与 select、poll 相比,epoll 的优势不能简化成“任何情况下都是 O(1)”。更准确的说法是:它维护注册的关注集合,并把已经就绪的事件交给调用方,避免每轮把完整 fd 集合从用户态传入并逐项扫描;最终成本仍会受到就绪事件数量及具体内核实现影响。Handler 这里需要长期监听 wake fd 和其他 fd,epoll 的接口形态很适合这种等待模型。

2. 插入新消息必须唤醒的原因

若线程正为一条“2 秒后执行”的消息等待,1 秒后另一线程插入一条立即消息,原来的 epoll_wait 仍可能按旧 timeout 再睡 1 秒。enqueueMessage 发现新消息成为队首且 mBlocked == true,便调用 nativeWake(mPtr):

plain text
sendMessage  → enqueueMessage    → needWake      → nativeWake(mPtr)        → NativeMessageQueue.wake()          → Looper.wake()            → write(mWakeEventFd, &inc, sizeof(uint64_t))

被唤醒后,next() 重新读取队首与当前时间。立即消息被返回;处理完后再为原延迟消息计算大约 1 秒的新 timeout。队列为空时则更直观:原先 -1 表示可以一直睡,任何新消息都需要一次 wake 才能让线程重新检查 Java 链表。

延迟 2 秒的消息在 1 秒时插入立即执行消息:enqueueMessage 触发 nativeWake,epoll 被唤醒后重新计算剩余等待时间

因此,延迟精度依赖“到期超时”和“队列改变时主动唤醒”共同维护。timeout 不是设置后便不可变的定时器,而是每次回到 next() 都会重新计算的等待上限。

六、IdleHandler:无立即消息时借用一个空档

1. 空闲的源码定义

MessageQueue 所说的空闲,不要求队列完全为空。只要 mMessages == null,或者队首消息尚未到期,即 now < mMessages.when,当前就没有立即消息可派发,IdleHandler 可以运行。

先看注册入口,是为了明确它只是把回调保存进 Java 队列,并不会单独创建线程或定时器。

java
// 代码位置 /frameworks/base/core/java/android/os/MessageQueue.java 第 115—122 行// 定义空闲回调注册方法。public void addIdleHandler(@NonNull IdleHandler handler) {    // 拒绝空回调。    if (handler == null) throw new NullPointerException("Can't add a null IdleHandler");    // 锁住队列,避免并发修改回调集合。    synchronized (this) { mIdleHandlers.add(handler); }// 结束注册方法。}

输入是 IdleHandler,输出没有返回值;状态变化只是 mIdleHandlers 多了一个元素。它何时运行,要由 next() 的空闲分支决定。

再看执行分支,是为了区分“延迟消息仍在队列里”和“现在有消息可执行”这两种状态。

java
// 代码位置 /frameworks/base/core/java/android/os/MessageQueue.java 第 368—403 行// 首次判断且当前没有立即消息时统计空闲回调。if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {    // 记录本轮准备执行的回调数量。    pendingIdleHandlerCount = mIdleHandlers.size();// 结束空闲判断。}// 没有空闲回调时准备继续阻塞。if (pendingIdleHandlerCount == 0) {    // 标记队列已阻塞并进入下一轮。    mBlocked = true; continue;// 结束无回调分支。}// 尚未创建临时数组时分配足够空间。if (mPendingIdleHandlers == null) {    // 数组容量至少为四。    mPendingIdleHandlers = new IdleHandler[Math.max(mIdleHandlers.size(), 4)];// 结束数组初始化。}// 制作快照,避免执行回调时直接遍历可变集合。mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);// 逐个执行本轮快照中的 IdleHandler。for (int i = 0; i < pendingIdleHandlerCount; i++) {    // 取得当前空闲回调。    final IdleHandler idler = mPendingIdleHandlers[i];    // 清除临时数组中的引用。    mPendingIdleHandlers[i] = null;    // 默认认为该回调不再保留。    boolean keep = false;    // 捕获回调自身抛出的异常。    try { keep = idler.queueIdle(); }    // 将回调异常记录为严重日志。    catch (Throwable t) { Log.wtf(TAG, "IdleHandler threw exception", t); }    // 返回 false 时从长期集合移除。    if (!keep) synchronized (this) { mIdleHandlers.remove(idler); }// 结束本轮空闲回调遍历。}

该分支的输入是当前时间、队首消息和空闲回调集合;输出是若干次 queueIdle() 调用。false 会移除回调,表示仅执行这一次;true 会保留它,未来再次进入空闲状态时仍有机会运行。若主线程一直有到期消息,空闲条件长期不成立,IdleHandler 也可能长期得不到执行,因此它不适合承载有确定完成期限的任务。

next() 判定主线程空闲后遍历 idleHandler:queueIdle() 返回 false 则移除只执行一次,返回 true 则保留每次空闲都执行

2. postDelayed 与 IdleHandler 的语义不同

postDelayed 承诺的是“不早于某个时间进入可执行候选”,IdleHandler 表达的是“队列暂时无立即消息时执行”。前者有时间条件,后者依赖队列负载。下面的用法示例同时保留两种选择,便于观察差异。

java
// 定义示例 Activity。public class MainActivity extends AppCompatActivity {    // 创建绑定主 Looper 的静态 Handler。    private static final Handler sHandler = new Handler(Looper.getMainLooper());    // 覆盖 Activity 创建回调。    @Override    // 接收保存的实例状态。    protected void onCreate(Bundle savedInstanceState) {        // 执行父类初始化。        super.onCreate(savedInstanceState);        // 加载页面布局。        setContentView(R.layout.activity_main);        // 延迟安排非首屏关键工作,减少与当前 UI 工作竞争。        // 不跟主线程渲染 UI 或 Activity 可见阶段竞争,延迟之后再执行。        // 向主线程队列投递延迟任务。        sHandler.postDelayed(new Runnable() {            // 覆盖任务执行入口。            @Override            // 定义延迟任务内容。            public void run() {                // 可在这里读数据库、埋点、采集日志或初始化其他模块。                // 读数据库、埋点、采集日志、初始化其他方法。            // 结束延迟任务。            }        // 设置至少延迟 1000 毫秒。        }, 1000);        // 注册主线程空闲时执行一次的回调。        // 主线程空闲时执行一次,可用于采集日志或性能分析。        // 取得主队列并添加 IdleHandler。        Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {            // 覆盖空闲回调。            @Override            // 返回是否保留此回调。            public boolean queueIdle() {                // false 表示执行一次后移除,true 表示保留。                return false;            // 结束空闲回调。            }        // 完成 IdleHandler 注册。        });    // 结束 onCreate。    }    // 定义点击事件中的耗时模拟。    public void testClick(View view) {        // 让当前主线程休眠 500 毫秒。        try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }    // 结束点击回调。    }// 结束示例 Activity。}// 标出示例中的版本信息边界。// 待核实:getQueue() 与 addIdleHandler 的准确 API level,素材中因版本不同提到 21/23;耗时用 Thread.sleep 模拟。

输入同样是主线程队列,但输出时机不同:延迟任务到期后成为普通可派发消息;IdleHandler 只在队列判定为空闲时回调。两者都在主线程执行,耗时任务放进去仍会阻塞 UI。所谓“不竞争首屏”仅表示调整执行时机,不代表获得后台线程能力。

七、消息屏障:改变 next() 的候选消息

同步屏障是 MessageQueue 链表中的特殊节点,其 target == null。普通消息入队时要求 target 非空,因而这个形态由队列内部的屏障接口创建。next() 遇到屏障后,不返回它,也不返回其后的同步消息,而是沿链表寻找第一条 isAsynchronous() 的消息。

消息屏障(target == null):同步消息被跳过,只有异步消息能越过屏障先执行,移除屏障后同步恢复

例如队列顺序为:

plain text
[屏障 target=null] → [同步 A] → [同步 B] → [异步 C] → [同步 D]

屏障存在时,C 可以越过 A、B 先执行;移除屏障后,同步消息恢复正常竞争。这个机制常用于 UI 帧调度,例如 Choreographer 相关链路让与帧有关的异步消息及时处理。

可以在 AOSP 中看到屏障的插入与移除实现。postSyncBarrier 获得一个 token,并把一条 target == null 的消息按 when 插入链表;removeSyncBarrier 按 token 定位并摘除它,必要时 nativeWake。下面这段用于证明“屏障节点确实是 target == null 的消息,并按 when 插入”(来源:frameworks/base/core/java/android/os/MessageQueue.java,android-6.0.0_r1):

java
// 代码位置 /frameworks/base/core/java/android/os/MessageQueue.java 第 462—489 行// 定义插入同步屏障的方法。private int postSyncBarrier(long when) {    // 锁住队列,保证屏障插入的原子性。    synchronized (this) {        // 为本次屏障分配一个自增 token,用于后续移除。        final int token = mNextBarrierToken++;        // 从对象池取得一条消息作为屏障节点。        final Message msg = Message.obtain();        // 标记消息已被使用。        msg.markInUse();        // 屏障也带有一个到期时间。        msg.when = when;        // 把 token 存进 arg1,供 removeSyncBarrier 定位。        msg.arg1 = token;        // 声明用于寻找插入位置的前驱节点。        Message prev = null;        // 取当前队首,作为扫描起点。        Message p = mMessages;        // 非零时间时沿链表跳过更早到期的消息。        if (when != 0) {            // 只要当前节点不晚于屏障时间就一直推进。            while (p != null && p.when <= when) {                // 记下前驱。                prev = p;                // 前进到下一个节点。                p = p.next;            }        // 结束时间扫描。        }        // 找到中部插入点时把新节点接到 p 前。        if (prev != null) {            // 新节点指向原 p。            msg.next = p;            // 前驱指向新节点。            prev.next = msg;        // 没有更早节点时成为新队首。        } else {            // 新节点指向原队首。            msg.next = p;            // 更新队首为新屏障。            mMessages = msg;        // 结束插入分支。        }        // 返回 token,供移除时匹配。        return token;    // 释放队列锁。    }// 结束插入屏障方法。}// 代码位置 /frameworks/base/core/java/android/os/MessageQueue.java 第 501—531 行// 定义移除同步屏障的方法。public void removeSyncBarrier(int token) {    // 锁住队列,保证移除的原子性。    synchronized (this) {        // 声明用于定位的前驱节点。        Message prev = null;        // 取当前队首,作为扫描起点。        Message p = mMessages;        // 跳过普通消息与 token 不匹配的屏障,直到找到目标。        while (p != null && (p.target != null || p.arg1 != token)) {            // 记下前驱。            prev = p;            // 前进到下一个节点。            p = p.next;        // 结束屏障定位。        }        // 找不到即视为重复移除或未插入。        if (p == null) {            // 抛异常提示 token 不存在。            throw new IllegalStateException("The specified message queue synchronization barrier token has not been posted or has already been removed.");        // 结束异常分支。        }        // 声明移除后是否需要唤醒。        final boolean needWake;        // 屏障不在队首时直接摘除即可。        if (prev != null) {            // 前驱越过目标节点。            prev.next = p.next;            // 不改变队首,因此无需唤醒。            needWake = false;        // 屏障就是队首时更新队首并判断唤醒。        } else {            // 直接把队首移到下一个节点。            mMessages = p.next;            // 新队首为空或是真消息时唤醒,让 next() 重新扫描。            needWake = mMessages == null || mMessages.target != null;        // 结束队首分支。        }        // 回收屏障消息对象。        p.recycleUnchecked();        // 队首变成真正的消息(而非屏障)时唤醒,让 next() 重新扫描。        if (needWake && !mQuitting) { nativeWake(mPtr); }    // 释放队列锁。    }// 结束移除屏障方法。}

这段代码印证了前面的机制:屏障节点确实是一条 target == null 的 Message,next() 遇到它会跳过同步消息、只找异步消息;removeSyncBarrier 按 arg1 == token 定位并摘除这个节点。素材本身没有给出这段方法体,这里是按课程所用 AOSP 分支核对的补充,作为外部权威资料佐证。

屏障也解释了 enqueueMessage 中较难读的一项条件:当线程已阻塞、队首是屏障、插入的是异步消息时,新消息可能成为下一条可执行消息,需要 nativeWake 让 next() 重新扫描。同步消息即使更早入队,也仍会被屏障挡住。

八、Message Printer

1. 打点位置在 dispatchMessage 两侧

主线程中的生命周期回调、点击处理和绘制相关工作,最终大量落在消息派发链上。要测量一条消息占用 Looper 线程多久,边界应放在 dispatchMessage 前后,而不是放在入队和出队之间;后者混入了排队等待时间。

看 Looper.loop() 是为了确认 Printer 打印与业务分发的严格相对位置。

java
// 代码位置 /frameworks/base/core/java/android/os/Looper.java 第 122—167 行// 定义当前线程的消息循环。public static void loop() {    // 取得当前线程关联的 Looper。    final Looper me = myLooper();    // 未调用 prepare 时不能启动循环。    if (me == null) throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");    // 取得该 Looper 的消息队列。    final MessageQueue queue = me.mQueue;    // 清除一次调用身份(AOSP 原文此处先调用一次、结果丢弃)。    Binder.clearCallingIdentity();    // 再次清除并记录当前线程身份令牌。    final long ident = Binder.clearCallingIdentity();    // 持续取消息并派发。    for (;;) {        // next 可能在 nativePollOnce 中阻塞。        Message msg = queue.next();        // 队列退出时结束循环。        if (msg == null) return;        // 使用局部变量,避免 UI 事件中更改 logger 造成前后不一致。        Printer logging = me.mLogging;        // 配置了 Printer 时记录派发开始。        if (logging != null) {            // 输出包含 target、callback 和 what 的开始标记。            logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);        // 结束开始日志分支。        }        // 把消息交给目标 Handler。        msg.target.dispatchMessage(msg);        // 配置了 Printer 时记录派发结束。        if (logging != null) {            // 输出包含 target 和 callback 的结束标记。            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);        // 结束完成日志分支。        }        // 再次取得当前线程身份令牌。        final long newIdent = Binder.clearCallingIdentity();        // 身份在派发期间被改变时告警。        if (ident != newIdent) {            // 输出身份变化的告警日志,方便追踪跨身份调用。            Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass().getName() + " " + msg.callback + " what=" + msg.what);        // 结束身份校验。        }        // 派发完成,回收这条 Message 回到对象池。        msg.recycleUnchecked();    // 进入下一次消息派发。    }// 结束 loop。}

输入是 queue.next() 返回的 Message,输出是 dispatchMessage 的业务副作用及两条可选日志。Printer 自身不改变调度顺序;它提供稳定的开始、结束标记,让监控代码计算单次派发耗时。

Looper.loop() 在 dispatchMessage 前后调用 mLogging(Printer):两条日志之间就是一次消息派发占用主线程的时间

2. 一个最小耗时监控器

java
// 导入主线程 Looper。import android.os.Looper;// 导入日志打印接口。import android.util.Printer; // 定义简单的性能监控类。public class PerformanceMonitoring {    // 检查当前 Handler 线程的一次消息派发耗时。    // 安装主 Looper 的消息日志监听器。    public static void monitoringTakeUpTime() {        // 为主 Looper 设置 Printer。        Looper.getMainLooper().setMessageLogging(new Printer() {            // 保存本次派发开始时间。            private long mStartTime = 0;            // 覆盖每条 Looper 日志的接收方法。            @Override            // 接收开始或结束标记。            public void println(String x) {                // 识别消息派发开始。                if (x.startsWith(">>>>>")) {                    // 使用当前墙上时钟记录起点。                    mStartTime = System.currentTimeMillis();                // 识别消息派发结束。                } else if (x.startsWith("<<<<<")) {                    // 计算本次派发经过的毫秒数。                    long cost = System.currentTimeMillis() - mStartTime;                    // 超过示例阈值时输出告警。                    if (cost > 60) {                        // 打印线程名和本次耗时。                        Log.e("MainThread", Thread.currentThread().getName() + " 当前线程有耗时:" + cost + "ms");                    // 结束阈值判断。                    }                // 结束日志类型判断。                }            // 结束 println。            }        // 完成 Printer 安装。        });    // 结束监控安装方法。    }// 结束监控类。}

输入是 Looper 生成的日志字符串,输出是超过阈值时的一条错误日志;内部状态 mStartTime 在开始标记到来时更新。它证明了“不改业务 Handler 也能覆盖主 Looper 派发”的可行性,但还不能直接证明具体是哪一行代码慢。

工程使用时还要处理几个边界:System.currentTimeMillis() 可能受系统时间校准影响,耗时测量更适合单调时钟;字符串前缀属于实现约定,升级版本时需要验证;全局 setMessageLogging 还可能覆盖已有 Printer,频繁日志也会增加测量扰动。素材给出的实现适合解释原理,不应未经评估直接当成完整线上方案。

九、卡顿与定位到方法

1. 一次慢派发为何可能错过一帧

60Hz 屏幕的一帧预算约为 1000 / 60 ≈ 16.7ms。在典型 UI 渲染链路中,vsync 驱动帧调度,主线程需要及时完成对应工作并让后续渲染链继续推进。如果某条主线程消息执行 500ms,期间会错过多次帧机会,画面表现为停顿。

主线程耗时超过 16ms vsync:界面数据未按时提交给 SurfaceFlinger,该帧不被渲染,观感即卡顿

“超过 16.7ms 就必定肉眼卡顿”并不严谨:刷新率可能不是 60Hz,一次派发也不等于完整一帧的全部预算;是否有动画、RenderThread 与 GPU 负载、系统调度都会影响结果。示例里的 60ms 是监控策略阈值,不是 Android 平台统一规定。SurfaceFlinger、应用侧帧调度及 buffer 交互的具体时序也随版本演进,本文只用它解释慢主线程与掉帧的关联。

2. Printer 发现“哪条消息慢”,线程栈帮助逼近“哪里慢”

Printer 的结束日志迟到,可以确认当前消息派发耗时过长。若要定位方法,可以在监控线程达到阈值时抓取主线程的 Thread.getStackTrace()。采样拿到的是某一时刻的调用栈:多次采样反复命中同一栈帧时,证据更强;一次样本可能恰好落在正常但短暂的调用上。

监听线程在达到阈值时抓取主线程 Thread.getStackTrace():多次采样命中同一栈帧更可信,可逼近哪个方法耗时

抓栈、格式化和写日志本身有成本,尤其不能在高频路径无限制执行。异常对象构造通常也涉及栈信息采集,这能提示其成本来源,但不同运行时版本的具体实现仍需结合对应 ART 源码验证。

还可以周期性向主队列投递轻量探针:探针长期未执行,说明主线程没有及时消费队列。这种方法能判断响应延迟,却无法单独定位阻塞方法。字节码插桩可以获得更细粒度的方法耗时,代价是构建复杂度、运行开销和数据量上升。

3. 借鉴 ANR 的“埋点—取消”思路

ANR 常被概括为:关键事件开始前安排一个超时检查,事件及时完成便取消检查;检查到期仍未取消,系统进入超时处理。Activity、Service 和 input 的超时链路并不完全相同,实际责任组件、超时值及实现随版本变化,不能把它们合并成一段固定源码。

借鉴 ANR 的埋炸弹/拆炸弹思路:派发前安排延时警告,完成即拆除,到期未拆才告警超时/阻塞

性能监控可以借鉴这种结构:派发开始时安排一个延时告警,派发结束时取消;告警真的执行时再抓主线程栈。这里容易产生一个误解:Looper.loop() 是死循环,却不会因为“存在死循环”直接触发 ANR。ANR 关注的是受监控操作是否在规定时间内完成以及应用是否仍能响应,而消息循环正是主线程正常工作的基础。

十、总结

Handler 通信的主链可以压缩成两条方向相反的路径:

plain text
发送:Handler.sendMessage   → MessageQueue.enqueueMessage   → needWake   → nativeWake   → write(mWakeEventFd) 消费:Looper.loop   → MessageQueue.next   → nativePollOnce(ptr, nextPollTimeoutMillis)   → NativeMessageQueue.pollOnce   → Looper.pollOnce / pollInner   → 返回 Message   → Printer 开始日志   → Handler.dispatchMessage   → Printer 结束日志

Handler 通信主链:发送链路改变队列与唤醒,消费链路负责等待与派发,mPtr 串联两层

  • 延迟消息:msg.when - now 形成下一次等待上限,epoll_wait 负责挂起;队列出现更早候选时,eventfd 写入触发 wake,Java 层重新计算 timeout。
  • 空闲回调:next() 发现队列为空或下一条消息尚未到期,执行本轮 IdleHandler 快照;返回值决定回调是否保留。
  • 主线程耗时监听:Looper.loop() 在 dispatchMessage 前后调用 Printer,监控器据此计算单次派发耗时,再结合延时告警或线程栈逼近具体阻塞

本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Android-Framework(十一)-彻底理解Handler的通信过程 · Yinchen 的博客