文章内容
一、Handler 的循环为何不烧满 CPU
Looper.loop() 中确实存在一个没有退出次数限制的 for (;;)。但“循环一直存在”和“CPU 一直执行循环体”是两回事。只要当前没有可立即处理的消息,Java 层就会把等待时间交给 nativePollOnce,native 层最终进入 epoll_wait。线程此时处于阻塞等待状态,不会反复读取队列。
这条等待链可以总结为3点:
- 延迟消息把“距离执行还剩多久”换算为
nextPollTimeoutMillis,由epoll_wait定时返回;如果更早的消息中途入队,nativeWake会提前唤醒等待并重算超时。 - 队列没有立即消息可取时,
MessageQueue.next()会先给IdleHandler一次执行机会,再决定是否继续阻塞。 - 一条消息从
MessageQueue返回后,Looper在dispatchMessage前后调用Printer,两次日志之间的时间就是这次派发占用主线程的时间。

二、全局概览:Java 队列如何落到 native epoll
1. 架构设计
Java 层参与消息通信的主要对象是 Handler、Looper、MessageQueue 和 Message:
Handler负责发送消息,并作为Message.target接收派发。MessageQueue用按when排序的单链表保存 JavaMessage。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 对象。

2. mPtr 跨层连接
先看 native 方法声明,划出 Java MessageQueue 与 native 世界的接口边界。
// 代码位置 /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 最终分别调用谁。
// 代码位置 /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。
// 代码位置 /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 的来源,并确认同步屏障在何处改变选中的消息。
// 代码位置 /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 最终确实传给内核等待接口,并找到主动唤醒的写端。
// 代码位置 /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 立即返回。

与 select、poll 相比,epoll 的优势不能简化成“任何情况下都是 O(1)”。更准确的说法是:它维护注册的关注集合,并把已经就绪的事件交给调用方,避免每轮把完整 fd 集合从用户态传入并逐项扫描;最终成本仍会受到就绪事件数量及具体内核实现影响。Handler 这里需要长期监听 wake fd 和其他 fd,epoll 的接口形态很适合这种等待模型。
2. 插入新消息必须唤醒的原因
若线程正为一条“2 秒后执行”的消息等待,1 秒后另一线程插入一条立即消息,原来的 epoll_wait 仍可能按旧 timeout 再睡 1 秒。enqueueMessage 发现新消息成为队首且 mBlocked == true,便调用 nativeWake(mPtr):
sendMessage → enqueueMessage → needWake → nativeWake(mPtr) → NativeMessageQueue.wake() → Looper.wake() → write(mWakeEventFd, &inc, sizeof(uint64_t))被唤醒后,next() 重新读取队首与当前时间。立即消息被返回;处理完后再为原延迟消息计算大约 1 秒的新 timeout。队列为空时则更直观:原先 -1 表示可以一直睡,任何新消息都需要一次 wake 才能让线程重新检查 Java 链表。

因此,延迟精度依赖“到期超时”和“队列改变时主动唤醒”共同维护。timeout 不是设置后便不可变的定时器,而是每次回到 next() 都会重新计算的等待上限。
六、IdleHandler:无立即消息时借用一个空档
1. 空闲的源码定义
MessageQueue 所说的空闲,不要求队列完全为空。只要 mMessages == null,或者队首消息尚未到期,即 now < mMessages.when,当前就没有立即消息可派发,IdleHandler 可以运行。
先看注册入口,是为了明确它只是把回调保存进 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() 的空闲分支决定。
再看执行分支,是为了区分“延迟消息仍在队列里”和“现在有消息可执行”这两种状态。
// 代码位置 /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 也可能长期得不到执行,因此它不适合承载有确定完成期限的任务。

2. postDelayed 与 IdleHandler 的语义不同
postDelayed 承诺的是“不早于某个时间进入可执行候选”,IdleHandler 表达的是“队列暂时无立即消息时执行”。前者有时间条件,后者依赖队列负载。下面的用法示例同时保留两种选择,便于观察差异。
// 定义示例 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] → [同步 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):
// 代码位置 /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 打印与业务分发的严格相对位置。
// 代码位置 /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 自身不改变调度顺序;它提供稳定的开始、结束标记,让监控代码计算单次派发耗时。

2. 一个最小耗时监控器
// 导入主线程 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,期间会错过多次帧机会,画面表现为停顿。

“超过 16.7ms 就必定肉眼卡顿”并不严谨:刷新率可能不是 60Hz,一次派发也不等于完整一帧的全部预算;是否有动画、RenderThread 与 GPU 负载、系统调度都会影响结果。示例里的 60ms 是监控策略阈值,不是 Android 平台统一规定。SurfaceFlinger、应用侧帧调度及 buffer 交互的具体时序也随版本演进,本文只用它解释慢主线程与掉帧的关联。
2. Printer 发现“哪条消息慢”,线程栈帮助逼近“哪里慢”
Printer 的结束日志迟到,可以确认当前消息派发耗时过长。若要定位方法,可以在监控线程达到阈值时抓取主线程的 Thread.getStackTrace()。采样拿到的是某一时刻的调用栈:多次采样反复命中同一栈帧时,证据更强;一次样本可能恰好落在正常但短暂的调用上。

抓栈、格式化和写日志本身有成本,尤其不能在高频路径无限制执行。异常对象构造通常也涉及栈信息采集,这能提示其成本来源,但不同运行时版本的具体实现仍需结合对应 ART 源码验证。
还可以周期性向主队列投递轻量探针:探针长期未执行,说明主线程没有及时消费队列。这种方法能判断响应延迟,却无法单独定位阻塞方法。字节码插桩可以获得更细粒度的方法耗时,代价是构建复杂度、运行开销和数据量上升。
3. 借鉴 ANR 的“埋点—取消”思路
ANR 常被概括为:关键事件开始前安排一个超时检查,事件及时完成便取消检查;检查到期仍未取消,系统进入超时处理。Activity、Service 和 input 的超时链路并不完全相同,实际责任组件、超时值及实现随版本变化,不能把它们合并成一段固定源码。

性能监控可以借鉴这种结构:派发开始时安排一个延时告警,派发结束时取消;告警真的执行时再抓主线程栈。这里容易产生一个误解:Looper.loop() 是死循环,却不会因为“存在死循环”直接触发 ANR。ANR 关注的是受监控操作是否在规定时间内完成以及应用是否仍能响应,而消息循环正是主线程正常工作的基础。
十、总结
Handler 通信的主链可以压缩成两条方向相反的路径:
发送: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 结束日志
- 延迟消息:
msg.when - now形成下一次等待上限,epoll_wait负责挂起;队列出现更早候选时,eventfd 写入触发 wake,Java 层重新计算 timeout。 - 空闲回调:
next()发现队列为空或下一条消息尚未到期,执行本轮 IdleHandler 快照;返回值决定回调是否保留。 - 主线程耗时监听:
Looper.loop()在dispatchMessage前后调用 Printer,监控器据此计算单次派发耗时,再结合延时告警或线程栈逼近具体阻塞
本文由 AgentWorkFlow 生成