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:负责发送消息和处理消息。你发消息,它处理。
我画了一张图,帮大家理清它们的关系:
你看,主线程启动时,系统会自动帮我们创建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就不能被回收。
怎么解决?我总结了三个方案:
- 使用静态内部类 + 弱引用(推荐)
- 在Activity销毁时移除所有消息
- 使用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