20、Handler与Looper:消息机制原理、Handler使用、子线程更新UI、内存泄漏处理

各位同学,今天我们来聊聊Android里一个绕不开的话题——Handler与Looper。

说实话,我刚入行那会儿,觉得Handler就是个“发消息的工具”。直到有一次线上崩溃,日志里全是“Only the original thread that created a view hierarchy can touch its views”,我才真正意识到,这东西背后藏着整个Android的消息驱动模型。

嗯,咱们今天就把这块硬骨头啃下来。

消息机制的核心:Looper、Handler、MessageQueue

先问大家一个问题:为什么我们不能在子线程里直接更新UI?

说白了,Android的UI控件不是线程安全的。如果多个线程同时操作同一个TextView,那画面简直不敢想——你改我改大家改,最后显示成啥样全靠运气。所以Android设计了一套单线程模型:所有UI操作必须在主线程完成

那子线程想更新UI怎么办?就得靠Handler把任务“扔”回主线程。

整个消息机制,其实就三个角色:

  • Looper:像个永动机,一直在循环里转,从MessageQueue里取消息。
  • MessageQueue:消息队列,先进先出,存着待处理的消息。
  • Handler:负责发送消息和处理消息。你发消息,它处理。

我画了一张图,帮大家理清它们的关系:

Android 消息机制核心流程 主线程(UI线程) Looper loop() 无限循环 MessageQueue 消息队列(FIFO) Handler handleMessage() 子线程(工作线程) 子线程 sendMessage() 更新UI控件

你看,主线程启动时,系统会自动帮我们创建Looper和MessageQueue。子线程想更新UI,就通过Handler发消息,消息进队列,Looper取出来,再交给Handler处理——整个过程都在主线程完成。

Handler的基本用法

先来个最简单的例子。我习惯在Activity里这样写:

// 方式一:匿名内部类(不推荐,容易内存泄漏)
Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                textView.setText("更新了");
                break;
        }
    }
};

// 子线程发送消息
new Thread(new Runnable() {
    @Override
    public void run() {
        // 模拟耗时操作
        SystemClock.sleep(2000);
        // 发消息给主线程
        handler.sendEmptyMessage(1);
    }
}).start();

这段代码能跑,但有个坑——我待会儿说。先看另一种写法:

// 方式二:使用post方法(更简洁)
handler.post(new Runnable() {
    @Override
    public void run() {
        textView.setText("更新了");
    }
});

post方法其实也是发消息,只不过系统帮你封装成了Runnable对象。我个人更喜欢这种方式,代码更直观。

子线程更新UI的正确姿势

刚才说了,子线程不能直接操作UI。那如果子线程里非要更新UI怎么办?

我遇到过新手同事,直接在子线程里写textView.setText(),然后崩溃了还一脸懵。嗯,这里给大家总结几种正确做法:

方法 适用场景 备注
Handler.sendMessage() 需要传递数据 通过Message.obj或Bundle传参
Handler.post() 简单UI更新 无需传参时最方便
View.post() 在任意线程更新该View View内部持有Handler引用
runOnUiThread() Activity内部 本质也是Handler封装

举个例子,用View.post()

new Thread(new Runnable() {
    @Override
    public void run() {
        // 耗时操作
        final String result = fetchData();
        // 直接通过View.post切回主线程
        textView.post(new Runnable() {
            @Override
            public void run() {
                textView.setText(result);
            }
        });
    }
}).start();

你看,连Handler都不用自己声明了,是不是很省事?

内存泄漏:Handler最大的坑

好,现在来说说刚才提到的坑——内存泄漏。

我曾经在一个项目里,用Handler做定时刷新。Activity退出后,Handler还在发消息,结果Activity泄漏了,GC回收不掉。用户来回进出几次,直接OOM。

⚠️ 为什么泄漏?

非静态内部类(包括匿名内部类)会隐式持有外部类的引用。Handler作为内部类,持有Activity引用。如果Handler的消息还没处理完,Activity就不能被回收。

怎么解决?我总结了三个方案:

  1. 使用静态内部类 + 弱引用(推荐)
  2. 在Activity销毁时移除所有消息
  3. 使用Lifecycle感知组件(Kotlin协程或LiveData)

先看第一种:

private static class SafeHandler extends Handler {
    private final WeakReference<MainActivity> mActivityRef;

    SafeHandler(MainActivity activity) {
        mActivityRef = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        MainActivity activity = mActivityRef.get();
        if (activity == null) {
            return; // Activity已销毁,直接返回
        }
        // 安全操作
        activity.textView.setText("更新了");
    }
}

// 使用
private SafeHandler mHandler = new SafeHandler(this);

第二种方案更简单粗暴:

@Override
protected void onDestroy() {
    super.onDestroy();
    // 移除所有未处理的消息和回调
    mHandler.removeCallbacksAndMessages(null);
}
💡 我的习惯

我一般两种方案结合用:静态内部类 + 弱引用是基础,然后在onDestroy里再调一次removeCallbacksAndMessages。双重保险,心里踏实。

Looper的进阶用法

说到Looper,大家可能觉得它只在主线程工作。其实子线程也可以有自己的Looper。

我记得有一次做图片批量处理,需要在子线程里串行执行任务。这时候就可以手动创建Looper:

// 子线程中创建Looper
class WorkerThread extends Thread {
    private Handler mHandler;

    @Override
    public void run() {
        Looper.prepare(); // 创建Looper和MessageQueue

        mHandler = new Handler(Looper.myLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // 处理任务
            }
        };

        Looper.loop(); // 开始循环
    }

    public Handler getHandler() {
        return mHandler;
    }

    public void quit() {
        mHandler.getLooper().quitSafely();
    }
}

注意:Looper.prepare()Looper.loop()必须成对出现。而且记得在不需要时调用quitSafely(),否则线程会一直运行。

避坑指南

最后,分享几个我踩过的坑:

  • Handler在子线程中创建会崩溃——因为子线程默认没有Looper。要么在主线程创建,要么先调Looper.prepare()
  • 不要用Handler做精确计时——Handler的消息处理受Looper循环影响,时间不准。用Handler.postDelayed()做倒计时还行,精确到毫秒就别想了。
  • Message.obj尽量用静态对象——避免在Message里持有大对象引用,防止内存泄漏。

我曾经在一个IM项目里,用Handler处理消息队列。结果因为消息积压,UI卡顿严重。后来改用HandlerThread + 队列限流才解决。嗯,那是另一个故事了。

好了,Handler与Looper的核心内容就这些。记住一句话:消息机制的本质,就是让多线程的“乱”,变成单线程的“序”。理解了这个,你就能用好它。


公众号:蓝海资料掘金营,微信deep3321