资讯动态

子线程Handler使用全解析:从Looper到HandlerThread

发布时间:2026/10/20 7:41:46 来源:尧图企业网站定制
1. 先搞清楚为什么子线程里 new Handler 会崩很多人第一次在子线程里写 Handler都是照搬主线程的写法new Thread(new Runnable() { Override public void run() { Handler handler new Handler(); } }).start();结果一运行直接崩了Logcat 里一行红字异常Cant create handler inside thread that has not called Looper.prepare()翻译过来就是你还没调用 Looper.prepare()这个线程里根本没有 LooperHandler 就没法创建。很多新手看到这个异常第一反应是“我是不是没导入包”其实和导入没关系这是 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( Cant 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 和 loopOverride 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 WeakReferenceMainActivity activityRef; WorkerHandler(WeakReferenceMainActivity 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 的高频报错和排查思路整理成速查表现象根本原因解决方法崩溃Cant 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 WeakReferenceonDestroy 里 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、又要逐条更新进度的场景比如批量下载、本地数据库迁移、文件扫描等直接套这个模板基本不会出问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑