本文源码基于 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 消息机制流程

一句话概括: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()
    }
}

排查下来,是两个问题叠在了一起:

  1. 内存泄漏:匿名内部类 Handler 持有 Activity,请求线程又持有 Handler。页面关闭后,Activity 依然被引用,无法回收。消息入队后,引用链变成 主线程 Looper → MessageQueue → Message → Handler → Activity。
  2. 对已销毁的页面更新 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 切回主线程的。