10.01 Handler 消息机制原理

2025/2/25 Android 开发基础

10.00 明确了异步编程的范式:工作线程干耗时活,切回主线程更新 UI。负责"切线程"的核心机制就是 Handler 消息机制——它是 Android 异步体系的地基:AsyncTask、HandlerThread、Activity 生命周期调度、ViewRootImpl 的重绘调度、图片库的线程切换,底层全是它。

本节把四大角色(Handler、Message、MessageQueue、Looper)与完整流转链路讲透。

# 一、整体架构:一个线程的"消息循环"

Handler消息机制全景示意图

# 动态演示:消息是怎么流转的

下面这张动画图循环演示了消息的完整生命周期(橙点 = 消息在四段路径上流转;队列槽位变橙 = 有消息占用;绿点 = 消息处理完被回收进对象池):

Handler消息机制动态演示图

对照动画分步理解(每一步对应后文的具体源码/小节):

动画阶段 发生了什么 对应实现
① 入队 工作线程调用 handler.sendMessage() / post(),消息带着 when 插入队列链表 MessageQueue.enqueueMessage()
② 出队 主线程 Looper.loop() 的 queue.next() 取到队头到期消息(没到期/没消息就休眠) MessageQueue.next() + epoll
③ 派发 msg.target.dispatchMessage(msg) → 回到 Handler 绑定的线程(即主线程)执行 Handler.dispatchMessage()
④ 回收 处理完后 recycleUnchecked() 把 Message 放回对象池,obtain() 的消息复用它 Message.recycleUnchecked()

注意动画里的两个细节:多个消息错峰入队(队列同时可能挂多条,所以才会有"延迟消息被前面耗时消息堵住"的现象);绿点回到对象池后再次进入流水线(同一个 Message 对象被反复使用,这就是 obtain() 的意义)。

四大角色与职责:

角色 职责 关键说明
Message 消息的载体 what/arg1/arg2/obj 携带数据,when 决定执行时间,target 指向发送它的 Handler
MessageQueue 消息队列 按 when 排序的单链表(不是队列数组),支持插入定时消息、按时间取消息
Looper 消息循环 loop() 死循环不断从队列取消息,取到就 dispatchMessage 派发;取不到则阻塞休眠
Handler 收发接口 sendMessage/post 发消息入队;消息派发回来后在Handler 绑定的线程执行

"切线程"的原理一句话:Handler 在哪个线程创建(准确说:绑定了哪个线程的 Looper),消息就在哪个线程被处理。工作线程发消息 → 消息进入主线程的队列 → 主线程的 Looper 取出来 → 在主线程执行 handleMessage。

# 二、主线程的 Looper 从哪来

主线程的 Looper 由系统准备,入口在 ActivityThread.main():

// ActivityThread.main()(源码主干)
public static void main(String[] args) {
    Looper.prepareMainLooper();      // ① 为主线程创建Looper
    ...
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    ...
    Looper.loop();                    // ② 进入消息循环(永不返回)
}
// Looper 的关键源码(主干)
public static void prepareMainLooper() {
    prepare();                        // new Looper(true) 存入ThreadLocal
}
public static void prepare() {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(true));
}
public static void loop() {
    final Looper me = myLooper();
    final MessageQueue queue = me.mQueue;
    for (;;) {                        // 死循环
        Message msg = queue.next();   // 可能阻塞
        if (msg == null) break;       // quit时返回null
        msg.target.dispatchMessage(msg);  // 派发给Handler(target即发送者)
        msg.recycleUnchecked();       // 回收进对象池
    }
}

三个要点:

  1. 每个线程最多一个 Looper,存放在 ThreadLocal(线程本地存储,线程间隔离)
  2. Looper.loop() 的死循环不占 CPU——queue.next() 取不到消息时在 native 层通过 epoll 机制休眠(nativePollOnce),有新消息或定时消息到期时由 nativeWake 唤醒。这就是"主线程永远在死循环却不会卡死"的答案
  3. 主线程 Looper 的死循环 ≠ ANR:ANR 是"某条消息(如输入事件、生命周期)没有在规定时间内被处理完",而不是循环本身。循环恰恰是保证消息能被逐条处理的前提(详见 10.03)

# 三、Message:消息与对象池复用

Message 推荐通过 Message.obtain() 获取而不是 new——它内部维护一个对象池(链表,默认上限 50 个),回收的消息被复用,避免高频发消息时的内存抖动(GC 压力,见 07.20 自定义View 的"避免在onDraw创建对象"同理):

// 消息的三种携带数据方式
Message msg1 = Message.obtain(handler, 1);        // what=1
msg1.arg1 = 100;                                   // 小整数用arg1/arg2
msg1.obj = resultObject;                           // 对象用obj(慎用跨进程场景)
handler.sendMessage(msg1);

// 更简洁:obtain的系列重载自动设置target
Message.obtain(handler, 1, 100, 0).sendToTarget();

// 携带Runnable(post方式,内部是msg.callback=runnable)
handler.post(new Runnable() {
    @Override
    public void run() {
        textView.setText("done");   // 在Handler绑定的线程执行
    }
});

handler.post 与 sendMessage 的区别只是消息类型:post 的消息执行 msg.callback.run();sendMessage 的消息执行 handleMessage()。两者都走同一个队列。

# 四、Handler 的创建与子线程陷阱

Handler 创建时会绑定当前线程的 Looper:

// 主线程中:主Looper已由系统prepare,可直接创建
Handler mainHandler = new Handler();

// 子线程中:没有Looper,直接创建会崩溃
new Thread(new Runnable() {
    @Override
    public void run() {
        Handler h = new Handler();  // 💥 Can't create handler inside thread
                                    //     that has not called Looper.prepare()
    }
}).start();

// 子线程正确姿势:先prepare再loop
new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();                     // 为当前线程创建Looper
        Handler threadHandler = new Handler(); // 绑定子线程Looper
        Looper.loop();                        // 进入循环(阻塞在此)
        // loop()之后的代码只在quit后执行
        threadHandler.getLooper().quit();     // 退出循环(通常在destroy时调用)
    }
}).start();

HandlerThread:官方封装的"带 Looper 的线程",省去手写 prepare/loop:

HandlerThread handlerThread = new HandlerThread("work-thread");
handlerThread.start();
// 串行后台任务队列:所有消息按序在该线程处理(相机操作、数据库串行写等场景常用)
Handler workHandler = new Handler(handlerThread.getLooper());

workHandler.post(new Runnable() {
    @Override
    public void run() {
        // 在work-thread线程执行,可做耗时操作
        String result = doHeavyWork();
        // 切回主线程更新UI
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                textView.setText(result);
            }
        });
    }
});

// 退出:不退出会导致线程泄漏
handlerThread.quitSafely();

跨线程切换的经典组合(上面的代码):后台 HandlerThread 处理耗时 → 主 Handler(Looper.getMainLooper())回 UI。这也是 AsyncTask、图片库内部的标准结构。

# 五、消息派发的分发优先级

dispatchMessage 的分发顺序(理解 post/send、Callback、handleMessage 三者关系的关键):

// Handler.dispatchMessage(源码主干)
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {                 // ① post()的消息:执行Runnable
        handleCallback(msg);                    //    msg.callback.run()
    } else {
        if (mCallback != null) {                // ② Handler(Callback)构造:执行callback
            if (mCallback.handleMessage(msg)) return;
        }
        handleMessage(msg);                     // ③ 兜底:子类重写的handleMessage
    }
}
优先级 来源 触发方式
① msg.callback handler.post(runnable) 执行 Runnable
② mCallback new Handler(callback) callback 返回 true 则拦截
③ handleMessage() 继承 Handler 重写 sendMessage 系列的常规路径

# 六、延迟消息与阻塞唤醒机制

# 6.1 postDelayed 的原理

postDelayed(runnable, 1000) 不是定时器,也不是延迟启动线程——消息立即入队,但 when = 当前开机时间 + 1000ms。队列按 when 升序排列,Looper 取消息时如果队头的 when 还没到,就休眠到到期时刻再取。所以:

  • 延迟是近似的(受主线程前面消息处理时长影响,误差取决于队列繁忙度)
  • 基于 SystemClock.uptimeMillis()(深度休眠不计入),适合 UI 调度而非精确计时

# 6.2 阻塞与唤醒:epoll 而非轮询

MessageQueue.next() 的核心调用链:

next() → 发现队头消息 when > 当前时间(或队列为空)
       → nativePollOnce(ptr, timeoutMillis)   // native层epoll休眠timeout毫秒
       ← 新消息入队或到期 → nativeWake()       // 唤醒
       → 返回该消息

同步屏障(sync barrier)(了解级别):postSyncBarrier 可以往队列插一个"屏障",使得队列只处理异步消息(setAsynchronous(true)),普通消息全部延后。系统用它保证 UI 绘制消息(Choreographer 的 VSYNC 调度)优先于普通消息,避免被大量任务消息挤兑——这就是屏幕绘制流畅的重要保障。应用层很少直接使用(API 隐藏)。

IdleHandler:队列空闲时(没有即刻要处理的消息)会回调 queue.addIdleHandler() 注册的任务,适合做"不着急"的初始化(启动优化中常用,见 10.03)。

# 七、典型陷阱与最佳实践

# 7.1 内存泄漏(最经典)

非静态内部类 Handler 隐式持有外部 Activity 引用;延迟消息在队列中存活期间,Message.target → Handler → Activity 这条链会让 Activity 无法回收:

public class DemoActivity extends AppCompatActivity {
    // 💥 非静态内部类:持有Activity引用,延迟消息未处理完时Activity泄漏
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            textView.setText("done");   // 引用了Activity的textView
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        mHandler.sendEmptyMessageDelayed(1, 60_000);  // 60秒后才处理,期间旋转屏销毁的Activity全泄漏
    }
}

正确姿势(静态内部类 + 弱引用 + 及时清理):

public class DemoActivity extends AppCompatActivity {
    // 静态内部类:不隐式持有Activity
    private static class SafeHandler extends Handler {
        private final WeakReference<DemoActivity> mActivityRef;  // 弱引用持有

        SafeHandler(DemoActivity activity) {
            super(Looper.getMainLooper());
            mActivityRef = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            DemoActivity activity = mActivityRef.get();
            if (activity == null || activity.isFinishing()) {
                return;   // Activity已销毁:丢弃消息,不更新UI
            }
            activity.textView.setText("done");
        }
    }

    private final Handler mHandler = new SafeHandler(this);

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 清空该Handler的全部待处理消息
        mHandler.removeCallbacksAndMessages(null);
    }
}

# 7.2 其他要点

  1. 显式指定 Looper:new Handler(Looper.getMainLooper()) 优于 new Handler(),避免"在哪个线程创建就绑哪个线程"的隐式行为(Java 9+ / API 30 后构造函数要求显式传入 Looper)
  2. 优先静态内部类 + HandlerThread/主 Looper,避免匿名 new Handler(){...}
  3. 子线程 Handler 记得 quit:HandlerThread 用完 quitSafely(),否则线程常驻泄漏
  4. Message 复用:用 Message.obtain(),不要 new;不要复用已在队列中的消息
  5. postDelayed 不做精确计时:UI 节奏用 Choreographer/属性动画,精确计时用 SystemClock + 自己的比较逻辑

# 八、常见问题

  1. "主线程 Looper 死循环为什么不会卡死应用?"——循环会阻塞休眠(epoll),CPU 占用极低;应用没有消息要处理时主线程本来就该待机。真正要保证的是每条消息别处理太久(ANR 话题,见下一节)。
  2. "子线程能不能 new Handler?"——先 Looper.prepare()(或直接用 HandlerThread),否则崩溃。
  3. "为什么 Handler 构造传的是 Looper 而不是线程?"——消息循环的实体是 Looper(线程通过 ThreadLocal 持有它),Handler 绑定 Looper 即等于绑定线程。
  4. "sendMessage 和 post 哪个好?"——等价,post 少写 handleMessage 分发、适合简单回调;sendMessage 适合多种消息类型用 what 区分的场景。

# 结束

本节系统讲解了 Handler 消息机制:四大角色(Message / MessageQueue / Looper / Handler)的职责与协作、消息循环的完整流转(入队 → 取消息 → 派发 → 回收)、主线程 Looper 的系统初始化与"死循环不卡死"的 epoll 休眠原理、Message.obtain() 对象池复用、子线程创建 Handler 的陷阱与 HandlerThread、消息派发三级优先级、postDelayed 的定时实现与同步屏障(了解级),以及最重要的 Handler 内存泄漏(静态内部类 + 弱引用 + removeCallbacksAndMessages)。Handler 是 Android 异步的通用语言,下一节看它最常见的搭档——线程池:10.02 线程与线程池。更多内容请参考官方文档:与界面线程通信 (opens new window)。