1. 先搞清楚为什么子线程里 new Handler 会崩
很多人第一次在子线程里写 Handler,都是照搬主线程的写法:
new Thread(new Runnable() { @Override public void run() { Handler handler = new Handler(); } }).start();结果一运行,直接崩了,Logcat 里一行红字异常:
Can't create handler inside thread that has not called Looper.prepare()翻译过来就是:你还没调用 Looper.prepare(),这个线程里根本没有 Looper,Handler 就没法创建。
很多新手看到这个异常第一反应是“我是不是没导入包”,其实和导入没关系,这是 Android 消息机制底层设计决定的。要理解这个问题,得先摸清楚 Handler、Looper、MessageQueue 这三者之间的关系。
打个比方:Looper 就像一条传送带,MessageQueue 是传送带上的队列,Handler 是投递员和操作工的组合。你要在传送带边上干活,前提是这条传送带已经铺好了、转起来了。而主线程在 App 启动时,系统已经帮它做好了 Looper 的初始化和启动;子线程则是一张白纸,没人帮你铺传送带,你自己得动手。
Handler 的构造器里会去取当前线程的 Looper,如果取不到就直接抛异常:
public Handler(@Nullable 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()"); } ... }所以问题的本质不是“怎么写代码”,而是“线程环境里没有准备 Looper”。
2. 正确姿势:三步在子线程里搭起消息循环
知道了原因,解法就很简单了。既然子线程没有 Looper,那我们就自己创建并启动一个。标准写法是三板斧:
Thread workerThread = new Thread(new Runnable() { @Override public void run() { // 第一步:准备 Looper Looper.prepare(); // 第二步:创建 Handler(此时能拿到当前线程的 Looper 了) Handler handler = new Handler(Looper.myLooper()) { @Override public void handleMessage(@NonNull Message msg) { // 处理消息 } }; // 第三步:启动消息循环 Looper.loop(); } }); workerThread.start();这三行代码,每一行都有明确的职责。
2.1 Looper.prepare() 做了什么
先看 prepare() 的源码逻辑:
public static void prepare() { prepare(true); } 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)); }这里有两个关键点。
第一,它创建了一个新的 Looper 实例。第二,它把这个实例存到了 ThreadLocal 里。ThreadLocal 的作用是“线程隔离”,每个线程访问的都是自己那份数据,互不干扰。所以 prepare() 本质上是在“当前线程”这个私有空间里初始化了一个 Looper。
如果同一个线程里连续调用两次 prepare(),第二次会直接崩溃。这也是新手容易踩的第二个坑:在已经调用过 prepare() 的线程里重复初始化。主线程就是一个典型的例子——系统启动阶段已经 prepare 过了,你在 Activity 里再手动调一次 Looper.prepare(),一样会崩。
2.2 为什么必须传 Looper.myLooper()
有些人会写new Handler()而不是new Handler(Looper.myLooper()),其实两者等价,因为 Handler 的无参构造器内部就是拿 Looper.myLooper()。但传参数的写法更明确,能提醒你:Handler 绑定的是“某个特定线程”的 Looper,不是随便绑的。
这里要引申一个很关键的认知:Handler 的消息处理逻辑跑在哪个线程?答案是——绑定的 Looper 所属的那个线程。也就是说,你在子线程里创建的 Handler,它的 handleMessage 就在子线程里执行。你不能指望这个 Handler 去更新 UI,因为 UI 只能在主线程操作。
很多初学者容易把 Handler 和主线程划等号,其实 Handler 只是一个消息分发工具,它依附于哪个线程的 Looper,就在哪个线程干活。
2.3 Looper.loop() 启动循环
public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; loop() cannot be prepared"); } final MessageQueue queue = me.mQueue; for (;;) { Message msg = queue.next(); // 可能阻塞 if (msg == null) { // 队列退出,loop 结束 return; } msg.target.dispatchMessage(msg); ... } }loop() 是一个死循环,会一直从 MessageQueue 里取消息,取到就发给对应的 Handler 去处理。只有当队列里返回 null(即 Looper 被 quit())时,这个死循环才会终止。
这也是很多人困惑的地方:主线程的 loop() 也是死循环,那手机为什么不会卡死?这涉及到 Android 的底层机制,后面单独讲。
3. 深入机制:Loop 死循环为什么不卡线程、不触发 ANR
这个问题值得展开讲,因为理解透了,你写子线程 Handler 的心态会完全不一样。
3.1 死循环和阻塞的区别
先明确一个概念:死循环不等于“忙等”。忙等是 CPU 一直在空转,疯狂消耗资源。而 Android 的 MessageQueue 取消息时,用的是 Linux epoll 机制。
Message next() { ... nativePollOnce(ptr, nextPollTimeoutMillis); ... }nativePollOnce 会让当前线程进入休眠状态,把 CPU 让出去。这时候线程挂起了,不消耗 CPU。什么时候被唤醒?当有新的消息插入队列时,会通过 native 层往 epoll 里写一个事件,线程立刻被唤醒,继续取消息。
所以整个 loop() 的工作状态是:有消息就处理,没消息就睡大觉。既不会卡死线程,也不会占用 CPU。这也是为什么主线程这么写没问题——归根结底是系统层面的设计了“阻塞-唤醒”而不是“忙轮询”。
3.2 那 ANR 是怎么回事
既然主线程是阻塞等待的,为什么还有 ANR?这要换个角度看。
ANR 的全称是“Application Not Responding”,触发条件不是主线程“在死循环”,而是主线程“长时间没有处理完某个事件”。比如你点了屏幕,Input 事件要发给主线程处理,主线程 Looper 也会收到这个消息,但前面排队了一堆耗时消息,导致它迟迟处理不到你的点击事件,超过了 5 秒,系统就判定 ANR。
换句话说,ANR 不是死循环造成的,而是消息积压、处理不过来造成的。这两个东西在表象上看起来都像“卡住”,但本质完全不同。
这个理解放到子线程 Handler 里同样适用:子线程的 loop() 也是阻塞唤醒机制,你不用担心它会拖垮性能。想通了这一点,后续用 HandlerThread 做后台任务才敢放心用。
3.3 子线程的 Looper 如何退出
主线程的 Looper 理论上永远不退出(App 退出时和进程一起没了),但子线程不行。如果子线程的消息队列处理完所有消息后就晾在那,线程会一直挂起等待状态,不会被回收。这在短生命周期场景里问题不大,但如果你是在一个常驻后台线程里搞消息循环,就必须设计退出机制。
退出方式是调用looper.quit()或looper.quitSafely():
quit():立即退出队列,清空所有消息,包括正在等待的消息。quitSafely():等当前队列里已存在的消息处理完毕后,再退出。
两者都会让 loop() 的 queue.next() 返回 null,从而结束死循环。
4. 更优雅的方案:使用 HandlerThread 封装
手动写 Looper.prepare() + Looper.loop() 的步骤是正确的,但代码不够整洁。每次都要写一个 Runnable 包三层,还容易忘掉某些步骤。Android 早就给开发者准备好了封装好的类——HandlerThread。
4.1 HandlerThread 内部做了什么
HandlerThread 继承自 Thread,它的 run() 方法已经帮你做了 prepare 和 loop:
@Override public void run() { mTid = Process.myTid(); Looper.prepare(); synchronized (this) { mLooper = Looper.myLooper(); notifyAll(); } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid = -1; }而且它还用 wait/notify 机制处理了一个很常见的并发问题:getLooper()在 Looper 还没创建完成时,会一直阻塞等待,直到 Looper 初始化完毕才放行。这样外部代码调用 getLooper() 拿到的绝对不是 null。
4.2 使用 HandlerThread 的完整示例
// 创建并启动 HandlerThread HandlerThread workerThread = new HandlerThread("image-download-thread"); workerThread.start(); // 获取子线程的 Looper,创建 Handler Handler workerHandler = new Handler(workerThread.getLooper()) { @Override public void handleMessage(@NonNull Message msg) { switch (msg.what) { case MSG_DOWNLOAD_IMAGE: // 执行耗时下载任务 downloadImage((String) msg.obj); break; } } }; // 发送消息到子线程执行 Message message = Message.obtain(); message.what = MSG_DOWNLOAD_IMAGE; message.obj = imageUrl; workerHandler.sendMessage(message);这段代码比手动版清爽太多。你不需要关心 Looper 的初始化和启动,只需要拿到 getLooper() 传给 Handler 即可。
4.3 生命周期管理和退出注意事项
HandlerThread 退出时,不能直接调用thread.interrupt()——那只会打断休眠状态,不会退出消息循环。正确做法是:
// 确保当前没有消息插队了 workerThread.quitSafely(); try { workerThread.join(); } catch (InterruptedException e) { e.printStackTrace(); }quitSafely() 的意义在于:你之前 sendMessage 进去的一堆消息,如果还没处理,会让它们按顺序处理完再退。而 quit() 不管这套,直接把队列清了。在实际业务里,比如批量上传任务,你肯定希望已经入队的任务能完整跑完,这时候就必须用 quitSafely()。
还有一个很实用的操作:HandlerThread 创建时可以设置线程优先级:
HandlerThread workerThread = new HandlerThread("background-task", Process.THREAD_PRIORITY_BACKGROUND);这样配上一个低优先级,可以避免后台任务抢占主线程和前台任务的 CPU 时间。
5. 子线程 Handler 使用避坑指南
把 HandlerThread 用起来之后,你会遇到一些更“现实”的问题,这些坑不是靠源码能解决的,需要从工程经验里总结。
5.1 第一个坑:内存泄漏
任何 Handler 都有可能造成内存泄漏,子线程 Handler 也一样。
场景是这样的:Activity 里定义了一个非静态内部类 Handler,它隐式持有 Activity 的引用。如果 Activity 销毁了,但 Handler 的消息队列里还有待处理的消息,或者 Handler 正在处理耗时任务,外部就没办法回收这个 Activity 实例。
子线程 Handler 尤其容易触发这个问题,因为后台任务往往比较耗时。你以为界面关闭了任务就不跑了,实际上 handleMessage 可能在 Activity 销毁后才执行到。
规范的写法是:把 Handler 定义成静态内部类,用 WeakReference 引用外部类:
private static class WorkerHandler extends Handler { private final WeakReference<MainActivity> activityRef; WorkerHandler(WeakReference<MainActivity> activityRef) { this.activityRef = activityRef; } @Override public void handleMessage(@NonNull Message msg) { MainActivity activity = activityRef.get(); if (activity == null) { // Activity 已被回收,直接丢弃这条消息 return; } // 正常处理 } }同时在 onDestroy() 里把消息清掉:
@Override protected void onDestroy() { super.onDestroy(); workerHandler.removeCallbacksAndMessages(null); }removeCallbacksAndMessages(null) 的名字里 “null” 是消息 token,传 null 表示移除所有消息和回调。这一步做完,Activity 销毁后消息队列干干净净,不会再有后续回调进来。
5.2 第二个坑:时序问题——loop 还没跑起来就发消息
这是我实际见过最多的一种问题。很多人写代码喜欢这样:
Handler workerHandler = new Handler(new HandlerThread("task").getLooper()); workerHandler.sendEmptyMessage(MSG_DO_SOMETHING);注意看,这里的 HandlerThread 并没有调用 start()。如果线程根本没启动,Looper 可能还没创建,getLooper() 会一直阻塞等待或者返回 null(取决于你调用的时机)。就算不崩,逻辑上也会出问题。
正确顺序必须是:先 start(),再 getLooper() 创建 Handler,最后再发消息。HandlerThread 源码里已经做了阻塞等待逻辑来保证 getLooper() 不返回 null,但你不 start() 是另一回事——线程没启动,等待就永远不会结束。
5.3 第三个坑:handleMessage 执行的时间不定
这一点要心里有数:用了 Handler 消息机制,你就把“顺序执行”的控制权交给了消息队列。放进队列的消息不一定会按你调用的顺序执行吗?其实在同一个队列里会按顺序执行,但“什么时候执行到你那条”是不确定的,取决于前面排了多少耗时的消息。
如果你的业务对时序敏感,比如任务 A 的结果要喂给任务 B,不要指望“先发 A 再发 B 就能保证 B 先拿到 A 的结果”。A 和 B 都是异步入队,B 的消息体里拿不到 A 的返回结果。这种情况要么把 A 和 B 拆成状态机,要么用回调把 B 的发送放在 A 的 handleMessage 之后。
5.4 第四个坑:多队列并发任务
再扩展一层。HandlerThread 的任务本质上是串行执行的——所有消息按顺序在同一个队列里逐个处理。如果你期望后台“并行”执行多个耗时任务,就不适合只搞一个 HandlerThread。
方案是按任务类型拆多个 HandlerThread,或者干脆换线程池:
ExecutorService executor = Executors.newFixedThreadPool(4);但要记住,线程池不是 Handler 消息机制,它是另外一套模型,没有 MessageQueue,任务之间天然并行。二者适用场景不同:HandlerThread 适合“串行处理大量离散消息”的场景,线程池适合“并行执行多个独立任务”的场景。
6. 常见问题与排查实录
我在做技术支持和带新人时,收集了一批子线程 Handler 的高频报错和排查思路,整理成速查表:
| 现象 | 根本原因 | 解决方法 |
|---|---|---|
| 崩溃:Can't create handler inside thread that has not called Looper.prepare() | 子线程没有 Looper | 创建 Handler 前先 Looper.prepare(),或改用 HandlerThread |
| 崩溃:Only one Looper may be created per thread | 同一线程重复调用 Looper.prepare() | 不要手动重复 prepare,交给 HandlerThread 管理 |
| getLooper() 返回 null 或一直卡住 | HandlerThread 没调用 start() | 先 start(),再 getLooper() |
| 消息全部堆积,处理不过来 | 子线程 Handler 处理的消息内部又串行耗时操作 | 拆分多 HandlerThread,或改线程池,或优化单个任务耗时 |
| Activity 销毁后 handleMessage 还在执行 | 没有移除消息,也没有弱引用 | 静态 Handler + WeakReference,onDestroy 里 removeCallbacksAndMessages(null) |
| 后台任务耗电快 | 线程优先级过高,或者 Looper 退出不及时 | 设置 THREAD_PRIORITY_BACKGROUND,任务完成后 quitSafely() |
| 用 Handler 更新 UI 崩溃:Only the original thread that created a view hierarchy can touch its views | 在子线程 HandleMessage 里直接改 UI | 子线程 Handler 处理完后,再通过主线程 Handler 发送结果到主线程更新 UI,或者用 post 到主线程 |
最后一个问题值得多说一句。很多人搞混了“子线程 Handler”和“主线程 Handler”的职责边界。如果你的需求是“子线程里发消息更新 UI”,正确的链路应该是:
// 子线程 Handler 处理耗时逻辑 workerHandler.handleMessage { String result = doHeavyWork(); // 通过主线程 Handler 回传结果 mainHandler.post { textView.text = result } }这样设计有两个好处:耗时逻辑不阻塞主线程,UI 更新又回到了主线程合法执行。这是整套 Handler 机制的经典用法,理解透彻了,你才算真正掌握子线程 Handler,而不只是会背几个 API。
在我的实际项目经验里,HandlerThread + 主线程 Handler 是最稳的组合。它兼顾了后台串行任务和 UI 更新的需求,逻辑清晰,代码也容易维护。遇到那种又要在后台做 IO、又要逐条更新进度的场景,比如批量下载、本地数据库迁移、文件扫描等,直接套这个模板基本不会出问题。