本文源码基于 Android 9(API 28)。
背景
Handler 几乎是每个 Android 开发者最早接触的“线程通信”工具:子线程拿到数据,handler.post {} 一下就能回到主线程更新 UI。
但用得多不代表理解得深。下面这些问题,你能不能都答上来?
- 一个线程可以有几个 Looper?几个 Handler?
- Handler 为什么能把任务从子线程切换到主线程?
Looper.loop()是死循环,为什么主线程没有卡死?postDelayed的延迟是怎么实现的?准吗?- 为什么说 Handler 容易导致内存泄漏?
这篇文章顺着源码把消息的“发送、入队、取出、分发”走一遍,答案自然就有了。
整体结构
Handler 机制由四个角色组成:
| 角色 | 职责 |
|---|---|
Message | 消息本身,携带 what、obj、when,以及处理它的 target(Handler) |
MessageQueue | 按执行时间 when 排序的单链表,负责入队和取出 |
Looper | 每个线程最多一个,不断从 MessageQueue 取消息并分发 |
Handler | 消息的发送者,也是处理者 |
一句话概括:Handler 把 Message 放进 MessageQueue,Looper 从 MessageQueue 取出 Message,再交回给 Handler 处理。 发送和处理可以在不同线程,但处理一定发生在 Looper 所在的线程。
原理
1. Looper:每个线程最多一个
Looper.prepare() 用 ThreadLocal 保证每个线程只有一个 Looper:
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
所以第一个问题的答案是:一个线程最多一个 Looper、一个 MessageQueue,但可以有任意多个 Handler,它们共用同一个队列。
主线程不需要我们手动 prepare,ActivityThread.main() 已经做了:
public static void main(String[] args) {
// ...省略
Looper.prepareMainLooper();
// ...省略
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
loop() 后面紧跟着一个异常,说明主线程的 Looper 正常情况下永远不会退出。
2. 发送:所有 send 和 post 最终都走到 enqueueMessage
无论是 sendMessage、sendMessageDelayed 还是 post(Runnable),最终都会调用:
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
// ...省略判空
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
两个细节:
msg.target = this:消息记住了是谁发的,分发时就交还给它。- 延迟消息的时间是
SystemClock.uptimeMillis() + delayMillis。uptimeMillis不包含设备深度睡眠的时间,这一点后面踩坑部分会提到。
3. 入队:按时间插入有序链表
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
// ...省略退出检查
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// 插到队头,如果 Looper 正在阻塞就唤醒它
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;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
MessageQueue 并不是真正的“队列”,而是一个按 when 从小到大排好序的单链表。新消息如果比队头还早,就插到最前面,并在 Looper 阻塞时调用 nativeWake 唤醒它。
4. 取出:没有消息就阻塞在 epoll
Looper.loop() 的核心循环很简单:
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// 返回 null 说明队列已经退出
return;
}
// ...省略日志和耗时统计
msg.target.dispatchMessage(msg);
// ...省略
msg.recycleUnchecked();
}
关键在 queue.next():
Message next() {
int nextPollTimeoutMillis = 0;
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// 遇到同步屏障,只找异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// 还没到时间,算出需要等多久
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
mBlocked = false;
// ...从链表摘下 msg
return msg;
}
} else {
nextPollTimeoutMillis = -1; // 没有消息,无限等待
}
// ...省略:执行 IdleHandler
}
}
}
nativePollOnce 最终调用到 Native 层 Looper 的 epoll_wait:
nextPollTimeoutMillis = -1:一直阻塞,直到nativeWake往 eventfd 写入数据把它唤醒nextPollTimeoutMillis > 0:最多阻塞这么久,到点自动醒来,这就是postDelayed的实现方式
这也回答了第二个问题:loop() 虽然是死循环,但没有消息时线程阻塞在 epoll 上,会让出 CPU,不会空转。 ANR 的原因从来不是 loop() 本身,而是某条消息处理得太久,导致后面的输入事件、绘制消息来不及处理。
5. 分发:三级优先级
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg); // 1. post(Runnable) 传入的 Runnable
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return; // 2. 构造 Handler 时传入的 Callback
}
}
handleMessage(msg); // 3. 子类重写的 handleMessage
}
}
post(Runnable) 其实是把 Runnable 包装成 Message.callback,所以它的优先级最高,也不会走到 handleMessage。
6. 为什么 Handler 能切换线程
很多人以为 handler.post {} 是把代码“搬”到了另一个线程,其实不是。线程切换靠的是两点:同一进程内的线程共享内存,以及消息由 Looper 所在的线程自己取出来执行。
先看 Handler 的构造函数:
public Handler(Callback callback, boolean async) {
// ...省略泄漏检查
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
Handler 创建时就绑定了一个 Looper,并拿到了它的 MessageQueue。传入 Looper 的构造函数(如 Handler(Looper.getMainLooper()))也是同样的逻辑,只是 Looper 由我们指定。
以子线程通过主线程 Handler 发消息为例,整个过程是这样的:
| 步骤 | 执行线程 | 发生了什么 |
|---|---|---|
1. Handler(Looper.getMainLooper()) | 任意线程 | Handler 记住主线程的 Looper 和 MessageQueue |
2. handler.sendMessage(msg) | 子线程 | 把 msg 插入主线程的 MessageQueue,必要时 nativeWake 唤醒主线程 |
3. queue.next() 返回 msg | 主线程 | 主线程从 epoll 中醒来,取出这条消息 |
4. msg.target.dispatchMessage(msg) | 主线程 | handleMessage 或 Runnable 在主线程执行 |
子线程做的事只有第 2 步:往一个共享的队列里放一条消息。enqueueMessage 内部用 synchronized 加锁,保证多个线程同时发送也是安全的。真正执行代码的是第 4 步,由主线程的 Looper.loop() 完成。
所以结论是:消息在哪个线程处理,取决于 Handler 绑定的 Looper,与从哪个线程发送无关。 这也解释了两个现象:
Handler(Looper.getMainLooper())不管在哪个线程创建,消息都在主线程处理- 无参的
Handler()绑定的是“创建它的线程”的 Looper,在没有 Looper 的子线程里创建就会抛出上面的异常
7. 两个容易被忽略的机制
同步屏障。 在 next() 里看到的 msg.target == null,就是同步屏障。它通过隐藏 API MessageQueue.postSyncBarrier() 插入,之后队列只放行异步消息,普通消息全部被挡住。ViewRootImpl.scheduleTraversals() 就是用它保证 VSYNC 到来时的绘制消息优先执行,不被业务消息插队。
IdleHandler。 队列暂时没有要处理的消息时,会回调注册的 IdleHandler,适合做一些不着急的初始化:
Looper.myQueue().addIdleHandler {
preloadSomething()
false // 返回 false 表示只执行一次,执行后移除
}
示例:在子线程里使用 Handler
子线程默认没有 Looper,直接 Handler() 会抛出 Can't create handler inside thread ... that has not called Looper.prepare()。推荐用系统提供的 HandlerThread,它已经帮我们准备好了 Looper:
class ImageLoader {
private val workerThread = HandlerThread("image-loader").apply { start() }
private val workerHandler = Handler(workerThread.looper)
private val mainHandler = Handler(Looper.getMainLooper())
fun load(path: String, callback: (Bitmap?) -> Unit) {
workerHandler.post {
val bitmap = BitmapFactory.decodeFile(path) // 在子线程解码
mainHandler.post { callback(bitmap) } // 回到主线程回调
}
}
fun release() {
workerHandler.removeCallbacksAndMessages(null)
workerThread.quitSafely()
}
}
注意 release() 里用的是 quitSafely():它会把已经到期的消息处理完再退出;quit() 则直接清空队列。
踩坑记录
1. 页面关了,请求回来还在更新 UI
这是我早期踩过的一个坑。详情页一进入就在子线程发起网络请求,结果通过 Handler 发回主线程,再弹出一个提示对话框。测试时发现,如果请求还没返回就按返回键关掉页面,过几秒应用会直接崩溃:
android.view.WindowManager$BadTokenException: Unable to add window
-- token android.os.BinderProxy@xxxx is not valid; is your activity running?
当时的代码大致是这样:
class DetailActivity : AppCompatActivity() {
private val handler = object : Handler() {
override fun handleMessage(msg: Message) {
AlertDialog.Builder(this@DetailActivity)
.setMessage(msg.obj as String)
.show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Thread {
val result = api.loadDetail() // 耗时几秒的网络请求
handler.obtainMessage(MSG_RESULT, result).sendToTarget()
}.start()
}
}
排查下来,是两个问题叠在了一起:
- 内存泄漏:匿名内部类 Handler 持有 Activity,请求线程又持有 Handler。页面关闭后,Activity 依然被引用,无法回收。消息入队后,引用链变成
主线程 Looper → MessageQueue → Message → Handler → Activity。 - 对已销毁的页面更新 UI:请求回来后,Handler 照常执行
handleMessage,在一个已经 finish 的 Activity 上弹对话框。这时窗口令牌已经失效,于是抛出BadTokenException。
泄漏本身不一定马上出问题,但它让“已经关闭的页面”继续收到回调,崩溃才是最终的表现。修复要从三个方面一起做:
class DetailActivity : AppCompatActivity() {
private val handler = UiHandler(this)
private var call: Call? = null // OkHttp 请求
// Kotlin 的嵌套类默认是静态的,不持有外部类引用
private class UiHandler(activity: DetailActivity) : Handler(Looper.getMainLooper()) {
private val ref = WeakReference(activity)
override fun handleMessage(msg: Message) {
val activity = ref.get() ?: return
// 页面正在关闭或已销毁,不再更新 UI
if (activity.isFinishing || activity.isDestroyed) return
activity.showResult(msg.obj as String)
}
}
override fun onDestroy() {
call?.cancel() // 1. 取消还没返回的请求
handler.removeCallbacksAndMessages(null) // 2. 清空队列里的消息
super.onDestroy()
}
}
| 措施 | 解决的问题 |
|---|---|
静态 Handler + WeakReference | 打断引用链,Activity 可以被回收 |
onDestroy 里取消请求、清空消息 | 页面关闭后不再有回调进来 |
更新 UI 前检查 isFinishing / isDestroyed | 万一回调还是来了,也不会操作失效的窗口 |
从那以后,我写任何“异步回调更新 UI”的代码,都会先问自己一句:页面关掉之后,这个回调还会不会来?
2. 同一个 Message 不能发两次
Message 在入队时会被 markInUse(),再次发送会抛出 This message is already in use。需要重复发送时,每次都用 Message.obtain() 或 handler.obtainMessage() 拿新对象。obtain() 会从一个最多 50 个的对象池里复用,比直接 new Message() 更省内存。
3. postDelayed 不是精确定时器
延迟基于 uptimeMillis,设备进入深度睡眠后时间会“暂停”;而且到点后还要排队等前面的消息处理完。所以 postDelayed 适合 UI 层的短延迟,不适合用来做“30 分钟后提醒”这类精确定时任务,这类需求应该交给 AlarmManager 或 JobScheduler。
总结
| 问题 | 答案 |
|---|---|
| 一个线程几个 Looper? | 最多一个,由 ThreadLocal 保证;Handler 可以有多个 |
| 为什么能切换线程? | 发送线程只是往共享的 MessageQueue 里放消息,由 Looper 所在线程取出执行 |
loop() 死循环为什么不卡死? | 没消息时阻塞在 epoll_wait,让出 CPU |
postDelayed 怎么实现? | 消息按 when 排序,nativePollOnce 带超时等待 |
| 为什么会内存泄漏? | Message.target 持有 Handler,非静态 Handler 持有 Activity;页面关闭后回调更新 UI 还可能崩溃 |
| 绘制消息为什么能插队? | 同步屏障 + 异步消息 |
Handler 的代码量不大,但它是理解 Android 主线程模型的钥匙。下一步可以继续看 Choreographer 如何借助同步屏障调度每一帧,以及 Binder 线程收到的系统调用是怎么通过 ActivityThread.H 切回主线程的。