Handler 机制与 Native 层 epoll 驱动
Handler 机制是 Android 开发中几乎每天都会接触的基础组件。大多数人知道它可以用来在子线程完成耗时任务后更新 UI,也知道它的四大核心组件(Message、Handler、MessageQueue、Looper)。
但这只是表层的 API 调用。如果你仅仅停留在 Java 层,你必然会产生一个经典的困惑:主线程的 Looper.loop() 是一个死循环,为什么这个死循环没有把系统卡死?更奇怪的是,当没有消息时,它去哪里了?
为了回答这个问题,我们必须穿透 Java 类的封装,潜入到 C++ (Native) 层,去看看 Linux 内核的 epoll 机制是如何支撑起整个 Android 操作系统的事件流转的。
为什么需要 Handler?
Android 规定,只有主线程(UI 线程)才能修改 UI。如果允许多个线程同时修改 UI,开发者就必须在每一个操作 UI 的地方加上 synchronized 等锁机制。这不仅会导致代码极其臃肿,更可怕的是极易引发死锁,严重拖垮系统的渲染性能。
Handler 就是为了取代锁而生的。 它的设计思想是:把多线程并发对资源的抢夺,转变为单线程串行的消息队列处理。从空间换时间,用队列化解了并发冲突。
比喻:邮局与传送带
- ThreadLocal & Looper:像是一个邮局里的分拣引擎(马达),每个邮递分拣中心(线程)只有一台。
- MessageQueue:是连接在马达上的传送带,上面放着一个个包裹。
- Message:就是包裹,里面装着信件内容(数据)和收件人地址(目标 Handler)。
- Handler:既是发件人,也是收件处的处理员。它负责把包裹扔到传送带上,并在包裹到达时负责拆开消费。
为了回答这些难以看透的底层黑盒迷思,本文将摒弃上来就丢一堆恶心源码的旧套路,采取**“从表象到内核,从 Java 层到 C++ 操作系统层”的循序渐进结构**,带你打通这套 Android 核心事件驱动体系的奇经八脉。
第一部分:Java 层的全景流转
在潜入深渊之前,我们先把上层 Java 开发者的日常调用链路串连起来。
1. 极简实战:日常开发中的三种标准 API 姿势
不论底层怎么玩花样,我们在日常撸码时跟 Handler 打交道通常只有以下三种方式:
姿势一:重写 handleMessage(常规寄件)
这是最经典的数据包裹传递方式,子线程发数据,主线程拆数据更新界面。
// 1. 在主线程创建 Handler,重写包裹拆解逻辑
Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == 100) {
textView.setText((String) msg.obj);
}
}
};
// 2. 在子线程打包并扔上传送带
new Thread(() -> {
Message msg = Message.obtain();
msg.what = 100;
msg.obj = "下载已完成";
handler.sendMessage(msg);
}).start();
姿势二:使用内部 Handler.Callback(解耦拦截机制)
这种方式不需要继承 Handler 写匿名内部类(可以避免一些强引用问题),且提供了一种消息拦截器的机制。
// 传入一个单独的 Callback 接口实现
Handler handler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
// 如果这里返回 true,代表这个包裹被我拦截消费了,底层的 handleMessage 就不会再执行了!
return false;
}
});
姿势三:使用 post(Runnable)(免打包,直接扔代码)
这是目前最流行的傻瓜式打法。我不发数据了,我直接把一大段想要在主线程运行的代码直接扔进传送带!
Handler handler = new Handler(Looper.getMainLooper());
new Thread(() -> {
// Runnable 本质上被偷偷塞进了一个空 Message 的 callback 字段里
handler.post(new Runnable() {
@Override
public void run() {
textView.setText("代码直接在主线程跑了!");
}
});
}).start();
知其然,更要知其所以然。这三种花式用法,究竟是怎么在同一套引擎里运转的?
2. 基础扫盲:普通线程如何才能拥有 Looper?
一个普通的 Java 线程默认是没有传送带和马达机制的,它就像一条直肠子,自上而下把 run() 里的代码执行完后,线程就彻底死亡回收了。如果我们想让一个线程持续存活,不断悬停在那接收并处理别处发来的任务,就必须给它手动安装一套 Looper 机制。
其实,在你们日常开发中,之所以能在主线程肆无忌惮地直接 new Handler() 而从来不报错,是因为在 Android 应用刚启动时,系统隐藏的核心类 ActivityThread.main()(相当于 Android 程序的真实入口)已经偷偷帮你为主线程调用了 Looper.prepareMainLooper() 和 Looper.loop(),强行给主线程上好了马达。
但在我们自己 new Thread() 创建的子线程中,如果你直接去 new Handler(),程序会当场崩溃抛出异常:Can't create handler inside thread that has not called Looper.prepare()。想要在子线程建收发站,你必须严格执行三步曲:
new Thread(() -> {
Looper.prepare(); // 1. 为当前子线程“安装”唯一的马达和传送带
Handler handler = new Handler(); // 2. 在装好马达的线程环境中创建发件人 (Handler)
Looper.loop(); // 3. 轰鸣启动马达,让当前子线程陷入死循环,不断从传送带取包裹处理
}).start();
那么这就引出了一个严谨的架构问题:既然我们需要给线程安装马达,怎样才能在源码级别保障**“一个线程绝对只能拥有唯一的一个 Looper 马达”**呢?如果不小心多次 prepare() 建立了多套传送带,消息投递绝对会错乱。
这里 Android 核心框架巧妙借用了 Java 原生的 ThreadLocal 机制:
// Looper.java
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
// 将 Looper 存入当前线程的专属内存区
sThreadLocal.set(new Looper(quitAllowed));
}
ThreadLocal 就像是每个线程的私有储物柜。当我们在主线程调用 sThreadLocal.set() 时,这个 Looper 就被锁在了只属于主线程的储物柜里。其他线程想通过 sThreadLocal.get() 也是拿不到主线程的 Looper 的,从而保证了线程和 Looper 的一对一绑定。
3. 通信闭环:Looper.loop 与 Handler 的分发
如果说 MessageQueue 是传送带,那么是谁在源源不断地把包裹从传送带上拿下来,并交给正确的人去处理呢?这就需要分拣引擎的核心——Looper 开始运转。
当你调用 Looper.loop() 时,实际上就是按下了分拣马达的启动开关。这台引擎进入了一个永不停止的循环工作流:
// Looper.java
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
// 开启死循环!(分拣引擎轰鸣般启动)
for (;;) {
// 1. 从传送带上取包裹。注意:这里可能会卡住(休眠)等待新包裹!
Message msg = queue.next();
if (msg == null) {
return;
}
// 2. 找到这个包裹的收件人(target 就是当初发消息的那个 Handler)
// 并且要求他自己去拆解处理
msg.target.dispatchMessage(msg);
// 3. 处理完毕,将包裹清空并回收到对象池,等待下一次复用
msg.recycleUnchecked();
}
}
包裹是如何精准送达的?
注意 msg.target 这个属性。每一个包裹(Message)在被抛上传送带(sendMessage)的瞬间,都会暗中把自己跟抛出它的发件人(Handler)绑定在一起:msg.target = this。
所以,当 Looper 引擎从传送带上取下这个包裹时,它根本不需要到处广播或者遍历寻找接收者。它就像是邮局的高级自动分拣机,直接认准包裹上的 target 地址,把包裹准确无误地递给了对应工位的业务员去处理。
这名业务员(Handler)收到包裹后,会在 dispatchMessage 中走最后的拆包流程,并将活儿委派给我们覆写的 handleMessage 方法:
// Handler.java
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
// 新手扫盲:大家平时爱用的 mHandler.post(new Runnable() {...}); 为什么也会进队列?
// 答案是底层的 post 源码会自动把那个 Runnable 对象塞进了 Message 的 callback 字段里!
// 取出包裹时,既然发现它自带了一段 Runnable 代码,就直接在此刻执行 runnable.run() 即可
handleCallback(msg);
} else {
// 如果是通过 mHandler.sendMessage() 纯发数据包裹过来的,走正常的处理分发逻辑
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
// 👇 最终来到了我们在 UI 线程覆写的这个核心方法
handleMessage(msg);
}
}
// 开发者最熟悉的业务代码边界:
Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
// 终于在主线程拿到了子线程传回来的数据,愉快地更新 UI!
textView.setText((String) msg.obj);
}
}
至此,数据(包裹)完美跨越了线程的鸿沟:**它在子线程被构建并扔上传送带,最终却在主线程被摘下拆卡。在这收与发的一瞬间,由于持有此 Handler 的线程是主线程,因此最终触发执行 handleMessage 的也是主线程。**这就是从多线程的混沌走向单线程秩序的完整闭环。
第二部分:休眠与唤醒的跨界黑魔法
既然 Looper.loop() 是一个跑在主线程的死循环,这必然会引发初学者的第一道警觉反问。
1. 致命一问:死循环为什么没有卡死主线程?
回到刚才的引擎开关,任何一个初学者都知道在主线程写一个长达整个应用生命周期的 while(true) 死循环是大忌。普通死循环会立刻耗竭 CPU 资源,导致系统假死触发 ANR (Application Not Responding)。那 Looper.loop() 为什么没有卡死整个 Android 系统?
秘密就在于 queue.next() 这一行。如果队列中没有消息,next() 方法并不会像疯狗一样不断循环空转消耗 CPU,而是会让自己休眠。这种休眠不是单纯的 Thread.sleep(),而是将控制权交给了底层的操作系统,此时该线程完全不消耗 CPU 资源。
当子线程调用 Handler.sendMessage() 把新消息放入队列时,又能瞬间唤醒主线程继续干活。
2. 下潜 C++ 层:epoll 的多路复用与休眠仓
当我们跨过 JNI 的边界,进入到 C++ 的世界,你会发现 Java 层的 MessageQueue 只是一个傀儡。在底层,有一个真正的 NativeMessageQueue,以及一个 C++ 层的 Looper。
真正的阻塞和唤醒逻辑,依赖的是 Linux 内核的机制。
epoll:Linux 的多路复用门卫
为了实现高效的阻塞和唤醒,Android 底层使用了 Linux 的 epoll 机制和 eventfd(在更老的 Android 版本中是 pipe 管道)。
比喻:大楼保安与按铃系统 如果一栋大楼里有很多个房间(File Descriptor, FD)可能有快递(事件)到来,保安该怎么检查? 笨流程 (select/poll):保安不停地在所有房间来回巡逻一圈,看谁家有快递。不仅累(耗 CPU),且房间多了巡逻太慢。 高效流程 (epoll):保安在大门口装了一套总机面板(epoll 控制面板),然后找个椅子睡大觉(阻塞等待)。只要任何一个房间的信箱被投递了快递,总机面板就会亮灯报警,并精准备注是哪个房间(唤醒并回调),保安直接过去处理即可。
1. 创建休眠仓:epoll_create 与 eventfd
在 C++ 的 Looper 构造函数中,它初始化了这个体系:
// system/core/libutils/Looper.cpp
Looper::Looper(bool allowNonCallbacks) {
// 1. 创建一个用于唤醒的 eventfd (一种轻量级的进程内/线程间通信的文件描述符)
mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
// 2. 创建 epoll 句柄(保安的总机面板)
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
// 3. 将 mWakeEventFd 注册到 epoll 控制面板上
struct epoll_event eventItem;
eventItem.events = EPOLLIN; // 监听可读事件
eventItem.data.fd = mWakeEventFd;
epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, &eventItem);
}
2. 进入休眠:epoll_wait
当 Java 层的 next() 发现没消息时,调用的 nativePollOnce 最终跑到了 C++ 层的 Looper::pollInner 方法中。在这里,它调用了内核函数让线程"睡着":
int Looper::pollInner(int timeoutMillis) {
// ...
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
// 核心代码:调用 Linux 系统函数 epoll_wait
// 此时线程让出 CPU,陷入睡眠,直到 timeout 超时 或 mWakeEventFd 被写入数据
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// 醒来后,处理唤醒事件
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
if (fd == mWakeEventFd) {
// 发现是自己的唤醒管线亮了,读取里面的数据(清空)
awoken();
}
}
// ...
}
此时的主线程,就如同陷入了深睡眠,哪怕循环一万年,也不占用任何一点点 CPU 的计算资源。
3. 跨度唤醒:Handler.sendMessage
另一边,当你的子线程在下载完图片后,调用 sendMessage() 将消息推入 Java 的队列中:
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
// ... 插入队列逻辑 ...
// 如果当时主线程正在休眠,就需要去踢醒它
if (needWake) {
nativeWake(mPtr);
}
}
}
底层的 C++ nativeWake 做了什么惊天动地的事情?它仅仅是往那个 eventfd 里面写了一个极小的数据:
void Looper::wake() {
uint64_t inc = 1;
// 向 mWakeEventFd 写入 8 字节的数据
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
}
这一句 write 触发了 Linux 系统的 I/O 机制。内核态马上发现 mWakeEventFd 变得可读,处于阻塞睡眠状态的 epoll_wait 瞬间被激活(保安的总机报警)。
于是,主线程苏醒,从 C++ 环境返回 Java 环境。此时的 queue.next() 成功拿到了排在队头的消息,并愉快地分发给 Handler 去处更新 UI。
3. 唤醒双保险机制:系统定时闹钟 vs 外部物理踹门
很多同学在阅读这段倒计时逻辑时会有一个巨大的疑问:如果主线程把控制权上交睡死了,底层的操作系统怎么算得出该在什么时间把它精准唤醒?要是唤醒晚了导致延迟消息不能及时处理怎么办?
这就涉及到底层的两套截然不同却又相互保驾护航的“唤醒双保险”,这也是 Android Handler 的设计极其迷人之处:
1. 自动定时唤醒(底层操作系统维护的精度闹钟)
请注意 MessageQueue 源码中那个做减法的数学计算逻辑:
final long now = SystemClock.uptimeMillis(); // 获取当前系统绝对时间
// ...
// 差值倒计时 = 队头包裹指定的执行时间 (when) - 目前的系统时间 (now)
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
当发现还没到第一个包裹的执行时间时,主线程通过减法算出了一个极其精确的等待毫秒跨度(差值)。然后它拿着这个倒计时的数字,传给了底层 Linux 系统的 epoll_wait(fd, events, timeout) 函数。
Linux 内核在底层是受硬件时钟频率中断驱动的,传入超时参数后相当于将这个任务托管给了操作系统。主线程确实主动休眠不沾一丁点 CPU 了,但 OS 内核会在倒计时毫秒数精确耗尽的那一瞬间,无情且极度准时地将该线程强行拉回到就绪执行态。主线程苏醒后回到 Java 的死循环中重新拿到当前时间 now——此时 now 自然刚好等同于包裹设定的 msg.when!这就是为什么 postDelayed 从来不需要也坚决不应该开启真实的计时器 Timer,因为系统内核提供的阻塞超时打断就是世上最廉价和准时的闹钟。
2. 被动紧急唤醒(能随时打破闹钟的物理警铃)
你可能会接着担忧:如果在 OS 内核倒计时 3 秒睡觉的过程中,用户网络请求回调了,突然塞进来一个现在立刻就要执行的加急包裹呢?底层 OS 闹钟还要等 3 秒才响,新急件不就严重被耽误了吗?
完全不会!这正是我们在上文中不厌其烦地强调**“快递员判断逻辑”**的核心原因:一旦被别的线程新塞入的急件包裹插队成了链表队头,不管保安曾经求操作系统设定了多久的长时间闹钟,子线程立刻就会一脚踢过去执行 nativeWake(mPtr)!
底层一旦由于调用 nativeWake 产生了写文件事件,处于监控睡眠状态的 epoll_wait 会瞬间察觉到 I/O 可读,它会立马提前抛出事件并强制结束自己那个未尽的倒计时,瞬间重返人间并回到 Java 层把刚刚那个急件包裹处理掉。
一个是 “按微小时间差做毫秒级倒计时的内核原生闹钟”,另一个是 “受急件刺激能随时一脚踹开中断休眠的物理终止开关”。一守一攻两者天衣无缝的配合,铸就了 Handler 永不遗漏又毫无内耗的事件分发底座。
第三部分:死磕核心源:MessageQueue 的收与发
现在,既然你已经具备了 Native 也就是 C++ 层 epoll 挂起与 nativeWake 踢醒的上帝视角,此时我们再回过头来啃极其考验功底的 MessageQueue 核心方法,一切曾经看似不讲道理的代码,都将变得顺理成章。
1. 包裹入队:基于时间优先级的单链表 (enqueueMessage)
大多数人会被 MessageQueue 这个名字误导,认为它底层是一个常规的队列数据结构。其实不然,它的内部是一个按执行时间排好序的单向链表。
每一条 Message 对象内部都有一个 next 引用,指向下一条消息。当我们在任何子线程调用 Handler.sendMessage() 或 postDelayed(Runnable, 1000) 时,所有的包裹最终都会汇聚到 MessageQueue.enqueueMessage() 方法中投递:
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
// 1. 并发投递必须加锁。可能有几十个子线程同时在往这一个传送带扔包裹
synchronized (this) {
msg.when = when; // 记录系统绝对时间,作为优先级依据
Message p = mMessages; // mMessages 始终指向链表的第一个队头元素
boolean needWake;
// 2. 队头插入检查:如果链表是空的,或者新消息的执行时间比队头消息还要早
if (p == null || when == 0 || when < p.when) {
// 直接取代旧队头,成为新的队头
msg.next = p;
mMessages = msg;
// 如果此时主线程正在沉睡等待(mBlocked = true),来了新队头,大概率需要唤醒它
needWake = mBlocked;
} else {
// 3. 中间插入:遍历这条单向链表,根据时间 when 找到合适的"空隙"插队
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
// 如果到了队尾,或者找到了一个执行时间比自己晚(大)的消息,就停下来
if (p == null || when < p.when) {
break;
}
// (省略部分异步屏障的唤醒判断逻辑)
}
// 断开指针并完成插入
msg.next = p;
prev.next = msg;
}
// 4. 跨线程踢醒:注意!此刻的执行流在【投递包裹的子线程】中!
// 如果发现主线程(收件人)正在沉睡(needWake=true),子线程就会通过底层踢醒它
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
这里的核心迷思:到底是谁在调用唤醒?在什么情况下才唤醒?
很多初看源码的人在这个地方会有巨大的抽离感,因为他们忘记了区分代码是运行在哪个线程的。弄懂这个跨线程交互,必须理清这三个身份:
-
谁在等包裹?(阻塞方) 主线程在调用
queue.next()拿不着属于自己当前该执行的任务时,它自己陷入了内核级的沉睡(此时设置了状态标志mBlocked = true)。 -
谁在扔包裹并执行唤醒?(投递方,即 enqueueMessage 执行所在线程) 当某个异步子线程完成下载后,调用了
sendMessage()。其实整个enqueueMessage塞入链表的动作,全部是跑在这个子线程里的! -
唤醒判断(极为讲究的性能克制) 子线程不但负责把包裹插上链表,还会精打细算地判断要不要去按系统唤醒铃铛(触发
nativeWake代价昂贵):- 如果此时
mBlocked == false:说明主线程根本没睡,正在满头大汗地拆平时积压的其他包裹。那子线程只需默默把新包裹放好离开即可,主线程忙完这一阵自然会继续去next()里拿包裹,完全不用多此一举去唤醒。 - 如果此时
mBlocked == true:还要一分为二看!- 如果新插的包裹最着急(插到了队头成了第一顺位):必须立刻干预唤醒!子线程会执行
nativeWake打破僵局。 - 如果新包裹不急(插到了其他并不着急的包裹后边):那就坚决不唤醒(
needWake依然等同于false)!因为既然新包裹都排到了后面,说明队头那个老包裹一定早就让正在沉睡的主线程定好了精准的操作系统闹钟(下文会讲epoll_wait的timeout参数)。连队头的老包裹都还没急到要打破闹钟喊醒它,你一个由于时间靠后半路插队进来的新包裹凭要去打断沉睡呢?等系统闹钟到点把保安叫醒后,保安自会按序拿走这个延后的包裹。
- 如果新插的包裹最着急(插到了队头成了第一顺位):必须立刻干预唤醒!子线程会执行
- 如果此时
这就好比:主线程保安在门卫室设定了一个下午 3点整 的系统闹钟睡着了(mBlocked=true)。
- 子线程快递员此时送进来一个要求下午 1点 处理的最急包裹,发现保安设的闹钟过点了,必会出事故,于是强行立刻按响了外部警铃震醒保安(子线程执行的被动唤醒:
nativeWake)。 - 假若送进来的是一个要等到下午 4点才处理的不急包裹,快递员一看既然不着急,坚决不按警铃,放下包裹静悄悄离开就行。反正在 3点整 的时候系统闹钟会自动把保安叫醒(内核级别的主动唤醒:
epolltimeout 到期),等保安下午 3点醒来处理完手头所有破事后,自然会顺理成章地摸到那个排在后方的“下午 4点”的包裹继续干活。
另外关于排序策略:
无论是要求立刻执行的消息,还是要求 10 秒后才执行的消息,都会被统一投递进入这条链表。插入时,通过比较 when(目标时间的绝对毫秒值),时间越紧迫的越靠近队头。主线程死循环 queue.next() 时,永远只用面对最着急的第一个包裹。
2. 引擎抽取核心:MessageQueue.next() 终极源码解析
这就是主线程抽干 CPU 血液或者安然入睡的核心咽喉部位:
// MessageQueue.java
Message next() {
// pendingIdleHandlerCount 仅在第一次循环时为 -1
int pendingIdleHandlerCount = -1;
// 刚进入这个方法时赋为 0。因为主线程此时初来乍到,绝不能上来就睡,
// 必须立刻带着 timeout 0 去过一遍底下的检查逻辑,看看队列里有没有现成的包裹。
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
// 在去休眠之前,如果发现有待处理的 Binder 跨进程命令,顺带清空处理一下
Binder.flushPendingCommands();
}
// 核心阻塞点:将上面算好的超时时间传给 C++ native 方法去休眠!
// 当 timeoutMillis = 0:系统内部函数立刻返回,完全不阻塞。
// 当 timeoutMillis = -1:一直死等,彻底交出 CPU 不再苏醒,直到其他线程执行 nativeWake 来物理踢醒。
// 当 timeoutMillis > 0:定一个高精度的内核闹钟,时间到期后系统内核会自动唤醒本线程。
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis(); // 获取当前系统时间的绝对毫秒值
Message prevMsg = null;
Message msg = mMessages; // 指向队伍里的第一个包裹
// (此处省略同步屏障找异步消息的高阶代码:如果遇屏障,msg 会顺藤摸瓜变成中间的那个异步包裹,而 prevMsg 则是它前面的节点...将在下文详解)
if (msg != null) {
if (now < msg.when) {
// 发现有包裹,但它的处理时间在未来,还没到!
// 【关键数学减法计算】:包裹执行时间 (when) - 目前系统时间 (now)
// 算出这个差值暂存起来。等当前这轮代码走完,进入【下一次 for 循环开头】时,
// 它就会成为上面 nativePollOnce 参数,让线程正好睡这段时间。
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; // 切断即将发出的包裹和链表的最后联系
return msg;
}
} else {
// 传送带上一无所有,暂存一个 -1 标记。
// 同样,进入下一轮 for 循环时,主线程就会带着 -1 执行 nativePollOnce 陷入彻底长眠。
nextPollTimeoutMillis = -1;
}
// 下方是真实的空闲处理(IdleHandler)与睡眠标志(mBlocked)逻辑:
// 如果是第一次进入空闲状态,且确实没包裹了(或包裹时间没到),就计算有多少个 IdleHandler 需要跑
if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// 如果空闲时也没有啥 IdleHandler 要执行的,那就真的无事可做了!
// 在此立下一面 Flag:mBlocked = true,告诉别人我要睡了
mBlocked = true;
// continue 回到外层的 for(;;) 循环开头,正式调用 nativePollOnce 陷入沉睡
continue;
}
// (如果 pendingIdleHandlerCount > 0,这里会继续往下跑 IdleHandler,跑完再重新进行一轮检查)
}
}
}
第四部分:Android 生态的特权后门
理解了常规循环如何跑通后,不要忘了为了支撑 60 帧极速渲染和各类初始化要求,系统给特定的操作开了 VIP 后门。
1. 高速通道:同步屏障与异步消息特权(VIP 绿色通道)
在上面关于链表的讲解中,我们说 Message 是严格按照时间 when 来排序的。但在实际应用中,有些消息拥有生死攸关的优先级——比如屏幕上的 UI 绘制请求。
如果一个点击事件导致大量的耗时消息拥堵在传送带上,此时系统的底层的 VSync 信号来了,要求立刻重绘屏幕(Choreographer 将发起绘图请求)。如果重绘请求要在普通消息列队后盲目等几百毫秒,那画面必定直接卡顿甚至严重掉帧。
为了解决这种紧急插队问题,Android 创造了同步屏障 (Sync Barrier) 与**异步消息 (Asynchronous Message)**组合发力的特权机制。
比喻:高速干线上的封路与救护车
- 普通消息:普通私家车,日常按时排队前行。
- 异步消息:救护车。只要给包裹或者 Handler 设置了属性
msg.setAsynchronous(true)。- 同步屏障:交警在主干道上放了一个不写目的地的特殊路障——实际上就是一个没有收件人(
target == null)的空包裹。
通过显式调用隐藏 API MessageQueue.postSyncBarrier(),可以向传送带队头塞入一个 target 为 null 的纯特权关卡包裹。当主线程死循环执行到 queue.next(),发现队头这第一个包裹竟然是一个 msg.target == null 的怪现象时,它立刻就会敏锐地意识到:交警封路了!
源码中对此的处理极其粗暴:
// MessageQueue.next() 中的隐藏秘密
if (msg != null && msg.target == null) {
// 发现同步屏障(交警封路),直接抛弃对普通消息的检索
// 顺着后面的链表不断往后找,直到找到第一辆带有异步(Asynchronous)标签的救护车
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
一旦遇到这种特殊包裹,主线程的提取逻辑立刻发生突变:所有的普通(同步)消息原地停止分发,主线程跳过它们,直接沿着单向链表向后搜索异步消息。一旦在后面发现了异步消息,直接越过前方所有的普通消息将其取走处理!
它是如何在真实系统源码中开启和关闭的?(以 UI 屏幕渲染为例)
在 Android 体系里最高频使用这个特权的,就是掌控整个屏幕绘制的 ViewRootImpl。这就是为什么你即使在主线程里塞了大量乱七八糟的同步消息,手机划起来依然相对顺滑的底层原因。
1. 何时开启(布置路障)?
当你按下了按钮,或者调用了 view.requestLayout() / invalidate() 触发 UI 刷新时,系统会迅速调起 ViewRootImpl.scheduleTraversals():
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 🚨 1. 紧急封路:调用底层 postSyncBarrier 发起同步屏障
// 这一步的核心源码仅仅是向连表里插入一个【msg.target == null】的特殊包裹!
// 并且它刻意没有触发 nativeWake() 唤醒,因为它只是个路障,不需要主线程立刻起来干活
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 🚑 2. 派出救护车:通过底层编舞者 (Choreographer) 发出一个【异步消息】
// 这个异步消息里包含的 Runnable,就是负责实际测绘的 mTraversalRunnable
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
封路一旦形成,主线程的控制权就彻底倾斜了! 即便此时普通包裹链表里还压着几十个没来得及处理的点击事件,它们也必须原地待命,确保上面发出的那个测绘 UI 的异步消息(即将到来的 VSync 信号)能畅通无阻地第一个到达 CPU 执行!
2. 屏障的有效时效与撤销(何时移除路障)?
你一定要在这个时候追问一个致命细节:如果路障一直在那,主线程岂不是彻底瘫痪了?这个撤销的方法 doTraversal() 到底是在什么神奇的时刻被执行的?
答案隐藏在刚才派出的那辆带有异步标签的“救护车”(mTraversalRunnable)身上。它并不是马上就会被拔腿执行的!
那辆救护车实际上在队列里静静等待着手机底层硬件(SurfaceFlinger)发出的下一个 VSync 信号(通常不到 16.6 毫秒就会来一次)。
一旦底层的 VSync 垂直同步电信号到达:
系统大管家 Choreographer 就会立刻让主线程提取那辆“救护车”(mTraversalRunnable)开始狂奔。此时便会触发我们熟悉的 doTraversal() 回调:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 🟢 1. 第一时间撤下路障!
// removeSyncBarrier 的源码就是在链表里遍历寻找刚才那个 token,找到后将其脱链剥离
// 路障一撤除,刚才被拦在后面的几十个点击事件终于可以在接下来的空隙中继续被分发
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// 2. 开始执行极其耗时的三大界面的生命周期
performTraversals(); // 底层包含 performMeasure、performLayout、performDraw
}
}
只有依靠这一套“封路 -> 救护车过关 -> 取消封路”的极速闭环,Android 才能如钢铁般死死捍卫住 每 16.6ms 必须刷新一帧 UI 的 VSync 底线,而不被任何不可控的普通业务逻辑拖拉至卡顿。
2. 空闲见缝插针的高手:IdleHandler
Handler 机制不仅可以在忙碌时排队,还能在闲暇时通知你!
IdleHandler 是 MessageQueue 提供的一个回调接口,允许我们在消息队列处于空闲状态时,去执行一些低优先级的辅助任务。
- 空闲的定义:当
MessageQueue是空的,或者队首消息还是一个很久以后才执行的延迟消息时,Looper在即将通过epoll_wait陷入深睡前,会去触发这个方法。 - 真实场景价值:App 极致启动提速。我们可以把各种大型第三方 SDK(不要求首屏立即可用的组件)封装进
IdleHandler。这样主线程会在彻底绘制完首屏界面,UI 用户可点击后,趁着喘息的机会(空闲了)去无声无息地执行 SDK 初始化。 - 返回值的控制语意:重写
queueIdle()返回false代表这也是个一次性任务,用完即弃;返回true则下一次主线程发呆时,还会再调用你(如:周期性的内存检测清理 gc 提示)。
极简实战:如何注册一个 IdleHandler?
// 获取当前主线程的 MessageQueue
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 当主线程干完所有正事,准备开始睡午觉前,会回调这里
// 执行非紧急耗时任务,例如:初始化重型 SDK、预加载下个页面的数据、执行打点上报
DelayedSdkManager.init();
// 返回 false:代表这个闲时任务只执行这一次,执行完就会被队列自动移除
// 返回 true:代表这是个长期巡逻任务,下次主线程一空闲还会叫它
return false;
}
});
第五部分:实战防坑
最后,我们将落回架构侧。不仅要理解核心的流转机制,在实际的高级架构开发与技术评审中,Handler 还有以下硬核考点和优化细节必须掌握。
1. 为什么坚决反对 new Message()?(享元模式复用池)
如果在极短时间内产生大量的 Message 对象,会导致内存条频繁上下跳动(内存抖动),这很容易触发 JVM 的 GC,导致主线程卡顿。
从源码中可以看到,Android 设计者采用享元模式 (Flyweight Pattern) 优雅地解决了这个问题:
- 对象池 (
sPool):Message类内部自带一个静态的单向链表sPool,容量上限为 50(MAX_POOL_SIZE)。 - 出池 (
obtain):我们应该总是调用Message.obtain()或Handler.obtainMessage()获取包裹。它会优先从静态对象池sPool的表头摘取一个干净的空包裹复用;只有当池子彻底掏空时,才会退化成new Message()。 - 回池 (
recycleUnchecked):在上文Looper.loop()的死循环中,包被签收处理完毕后,并不需要系统垃圾回收,框架会主动调用msg.recycleUnchecked(),它会将包裹里的数据抹除清空,并且重新挂回到sPool链表的表头。
2. 内存泄漏的终极真相
“Handler 会造成内存泄漏”这句话是被无数新人奉为圭臬的教条,但很多人解释不清具体的原因链。内存泄漏的本质是:长生命周期的对象持有了短生命周期对象的引用。
如果你在 Activity 中写了一个匿名内部类(或非静态内部类):
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 隐式持有了外部类 MainActivity.this 的引用
}
};
这就构建了一条极为隐秘且致命的引用链:
主线程的 ThreadLocal -> 唯一的 Looper -> MessageQueue 传送带 -> 延时的 Message 包裹 -> 发送包裹的 Handler -> 所在的 Activity!
假设你通过 mHandler.postDelayed(..., 10000) 放了一个 10 秒的延时任务,然后用户在第 2 秒就退出了这个界面(Activity.finish())。但由于那个包裹还要在传送带上再待 8 秒,所以 Message 不死,Handler 不死,整整一坨庞大的 Activity 视图体系也绝不会被系统垃圾回收(被强引用牵连着),从而发生了典型的内存泄漏。
正确的架构防坑姿势(二选一):
- 静态化斩断引用:把
Handler改写为static class(静态内部类不持有外部引用),如需操作 Activity 则在构建时传入并使用弱引用(WeakReference<Activity>)。 - 生命周期兜底:在
Activity的onDestroy生命周期中,绝不要忘记写一句mHandler.removeCallbacksAndMessages(null);,像倒垃圾一样把传送带上凡是贴着自己名字的包裹全部焚毁。
3. 终极架构反勘:Google 为什么在底层选用了 epoll?
为什么 Google 的工程师不直接在 Java 层使用 Object.wait() 和 Object.notify() 来做这个线程挂起呢?为什么非要费劲跑到 C++ 层用 epoll?
这就是架构的宏大视角:主线程需要监听的,不仅有 Java 层发来的消息,还有底层的各种事件。
用户的屏幕触摸事件(InputEvent)、键盘输入、甚至不同进程间由于 Binder 调用产生的反馈,都在内核态产生。epoll 面板上不仅能挂载我们自己的 mWakeEventFd,还能同时挂载 Socket 的 FD、各种传感器的 FD。
主线程在这个死循环中,只要监视着一个统一的 epoll 面板,谁发生了事件,它就能迅速响应谁。Handler 机制表面上看来是用于多线程更新 UI 的工具,但从更底层看,它是整个 Android 应用生命周期和事件响应的心脏起搏器。每一次点击、每一次页面重绘,都伴随着底层 write 到 eventfd 的轻微震动,驱动着这个精悍而庞大的操作系统滚滚向前。