HandlerThread源码解析:线程与消息循环的串行化机制
1. HandlerThread到底是什么值得专门写一篇解析先说结论HandlerThread是Android系统提供的一个自带Looper的线程类继承自Thread。它解决的是一个很常见的痛点——你需要在子线程里处理任务但这个线程需要反复使用、需要支持消息队列、需要能优雅退出。很多人一开始接触HandlerThread是在AsyncTask还火的时候或者在处理图片压缩、数据库操作、文件读写这些场景里。我当时第一次看到这个类其实挺困惑的这不就是Thread里套了个Looper吗为什么要单独封装一个直到我后来翻源码、在真机上压测、线上出过几次bug之后才真正理解这个类设计得有多巧妙——它把线程和消息循环这两件事绑定在了一起而且通过一个很经典的wait/notify机制解决了线程安全初始化的问题。适合看这篇文章的人Android初级开发者想搞懂Handler完整机制的人准备面试的人以及那些在项目里用了HandlerThread但说不清它为什么吐出一个空Looper会crash的人。我不打算从使用文档的角度复述API而是直接带你一行一行拆源码再讲清楚它和Handler、Looper、MessageQueue之间的关系最后聊聊真实项目里的使用姿势和坑。有一点先明确HandlerThread的核心价值不是让子线程能收消息而是让一个线程稳定地、串行地、可控制地处理消息。它天然支持消息队列的排队机制所以特别适合做串行任务队列、延时任务、后台轮询这类需求。2. 核心机制拆解Looper、Handler和MessageQueue是如何被串起来的在看源码之前先把基础概念打通。很多人搞不清这三者的关系导致看HandlerThread源码时一头雾水。2.1 从一次ThreadLooper的手写说起假设你需要在子线程里接收主线程发来的任务最原始的做法是这样Thread thread new Thread(new Runnable() { Override public void run() { Looper.prepare(); mHandler new Handler(Looper.myLooper()); Looper.loop(); } }); thread.start();这段代码本身就能跑但有两个问题第一Looper初始化发生在子线程run方法内外部比如主线程如果想立刻拿到这个Handler并sendMessage可能会拿到null——因为thread.start()返回的时候子线程可能还没执行到Looper.prepare()第二这个线程跑起来就永远停不下来如果调用quit()外部拿到的还是那个已经死掉的Handler继续发消息会怎样消息会堆积在MessageQueue里但没有人去取。这就是HandlerThread要解决的根本问题保证外部能安全地拿到一个已初始化好的Looper同时提供干净的退出机制。它用同步锁和wait/notify机制把Looper初始化完成这个事件变成了一个可阻塞的、可感知的边界——外部调用getLooper()时如果Looper还没准备好就阻塞等待一旦准备好了立刻唤醒并返回。2.2 消息循环的完整链路以及ThreadLocal扮演的角色HandlerThread之所以能和Handler无缝配合靠的是Android消息机制里一个关键设计——Looper是线程局部存储的。每个Looper通过ThreadLocal绑定到创建它的线程上Handler在创建的时候通过Looper.myLooper()拿到当前线程的Looper之后往这个Looper对应的MessageQueue里发消息而消息最终又是在创建Looper的那个线程里被取出来执行的。这条链路我用餐厅例子打比方Looper是后厨的传菜员MessageQueue是传菜口的排队架Handler是顾客下单的窗口。顾客主线程或其他线程通过Handler下单菜品进入排队架传菜员也就是Looper从排队架里取单并交给厨师目标线程的代码处理。HandlerThread就是那个专门招聘的传菜员——它保证后厨一直有人在传菜而且每一道菜都是按顺序做的不会插队。要确认一个线程有没有Looper可以直接调用Looper.myLooper()。如果返回null说明这个线程没有消息循环这时候new Handler会直接抛RuntimeException。HandlerThread内部做了什么它在run方法里主动调用了Looper.prepare()所以这个线程天然就带消息循环。2.3 HandlerThread的五个核心方法先整体扫一遍HandlerThread的源码很短整个类加上注释不超过150行。我把它的关键结构先列出来然后逐段精读。public class HandlerThread extends Thread { int mTid; volatile Looper mLooper; private Handler mHandler; public HandlerThread(String name) { super(name); mTid -1; } public HandlerThread(String name, int priority) { super(name, priority); mTid -1; } protected void onLooperPrepared() { } Override public void run() { mTid Process.myTid(); Looper.prepare(); synchronized (this) { mLooper Looper.myLooper(); notifyAll(); } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid -1; } public Looper getLooper() { if (!isAlive()) { return null; } synchronized (this) { while (isAlive() mLooper null) { try { wait(); } catch (InterruptedException e) { } } } return mLooper; } public boolean quit() { Looper looper getLooper(); if (looper ! null) { looper.quit(); return true; } return false; } public boolean quitSafely() { Looper looper getLooper(); if (looper ! null) { looper.quitSafely(); return true; } return false; } public int getThreadId() { return mTid; } public Handler getThreadHandler() { if (mHandler null) { mHandler new Handler(getLooper()); } return mHandler; } }看到这里你大概已经明白我在说什么了HandlerThread其实就是一个在run方法里完成Looper初始化、然后进入消息循环的Thread关键在于它是如何解决外部拿不到Looper这个问题的。3. 源码逐段精读从run()到getLooper()再到quit()的每一个细节现在进入正题我按源代码的关键节点逐一拆解每一段都结合原理、字节码层面和实际应用来分析。3.1 run()方法prepare、初始化、设置优先级、onLooperPrepared、loopOverride public void run() { mTid Process.myTid(); Looper.prepare(); synchronized (this) { mLooper Looper.myLooper(); notifyAll(); } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid -1; }这段代码的执行顺序非常讲究每一行都有它的理由不能乱调。第一步记录线程ID。Process.myTid()拿的是内核线程ID和Java层的Thread.getId()不同它在排查线程状态时很有用——在线程dump里看到的是native tid如果你需要精确对应到某个HandlerThread可以用HandlerThread.getThreadId()来匹配。第二步Looper.prepare()。这一步在new Handler之前必须完成因为它会创建一个Looper并存入ThreadLocal。这一步执行完当前线程才成为一个有Looper的线程。注意这里调用的prepare是Looper的静态方法它内部会先检查ThreadLocal里是不是已经有Looper了如果有就直接抛异常——这就是为什么一个线程只能调用一次Looper.prepare()同一个HandlerThread的ThreadLocal只能存一个Looper。第三步是整个设计最精髓的地方先把mLooper Looper.myLooper()赋值给成员变量然后notifyAll()。这里为什么要加synchronized因为getLooper()方法在另一个线程通常是主线程里被调用时会阻塞等待而这个mLooper赋值必须和getLooper里的wait形成正确的同步关系。如果不加锁getLooper可能读到null或者读到半个状态的mLooper虽然volatile能保证可见性但wait/notify必须配合synchronized的监视器才能用。所以这段是将Looper初始化完成通过锁和条件变量广播出去。第四步Process.setThreadPriority(mPriority)。这是把线程优先级设置放在onLooperPrepared之前的原因——你可以在onLooperPrepared里做一些初始化操作这时候线程优先级已经是设置好的了不会出现优先级生效晚于初始化任务的情况。我见过有些人在外部手动设置HandlerThread的线程优先级其实完全没必要构造方法里传priority参数就行。第五步onLooperPrepared()。这是一个空方法是系统留给子类的扩展点。比如你可以在里面初始化一个Handler或者做一些需要在Looper准备好之后才能做的操作。这个回调的时机是Looper已经prepare好了、但loop还没开始所以此时发消息还是安全的消息会排队等待loop消费。第六步Looper.loop()。这里是阻塞调用它会拿到当前线程的Looper不断从MessageQueue里取消息执行。loop()一旦返回说明Looper已经退出循环了这时候执行最后一步mTid -1撤销线程ID记录。注意run方法执行到这里就结束了线程也就随之结束了。有一个很多文章没强调的点如果Looper.loop()一直没有退出那么run方法里Looper.loop()之后的代码永远不会执行。所以mTid -1实际上是在线程退出后执行的它标记着这个线程已经死了。3.2 getLooper()的等待唤醒机制以及为什么不能用轮询public Looper getLooper() { if (!isAlive()) { return null; } synchronized (this) { while (isAlive() mLooper null) { try { wait(); } catch (InterruptedException e) { } } } return mLooper; }这个方法的设计目标是确保外部调用者拿到的Looper一定是非空且已初始化完成的。它做了双重检查先看线程是否存活isAlive如果线程已经死了说明run方法都执行完了Looper肯定已经不在了直接返回null避免后续调用崩溃如果线程还活着则进入同步块循环等待mLooper不为空。为什么用while循环而不是用if这是一个经典的并发编程问题——需要在wait被唤醒后重新检查条件是否满足。因为wait可能被中断或者被虚假唤醒即使notifyAll被调用了也可能有其他线程先抢到锁修改了状态。while (isAlive() mLooper null)确保唤醒后重新检查一次条件不满足就继续等这是wait/notify的标准范式。有人可能会问为什么不直接用自旋轮询比如while (mLooper null) { Thread.sleep(1); }原因有两个第一轮询会不必要地占用CPU时间片wait会让出CPU真正地阻塞在那里不消耗CPU资源第二wait/notify是线程间协作的经典机制它在语义上一层唤醒、精确传送比轮询更符合事件通知的本质。你可以在线程启动后马上调用getLooper()它不会浪费时间空转而是立刻进入等待状态直到run方法执行到notifyAll。这个等待机制还有一个细节getLooper()只能被调用一次才有意义吗不是。你可以在线程已经初始化完成之后再次调用getLooper()它会直接跳过while循环立刻返回mLooper。所以getLooper()既保证了第一次调用的线程安全又保证了后续调用的高效。3.3 quit() vs quitSafely()暴力退出的代价public boolean quit() { Looper looper getLooper(); if (looper ! null) { looper.quit(); return true; } return false; } public boolean quitSafely() { Looper looper getLooper(); if (looper ! null) { looper.quitSafely(); return true; } return false; }这两个方法都调用的是Looper本身的退出方法差别在于MessageQueue处理消息的方式。quit()对应MessageQueue.quit(false)这个方法是立即退出——它会把消息队列里还没有处理的所有消息全部清空不论你是普通消息、同步消息、延时消息一律丢弃。然后Looper.loop()返回线程结束。所以这种方法适合我已经不在乎队列里的任务了赶紧关门的场景。quitSafely()对应MessageQueue.quit(true)这个方法是安全退出——它只会把队列中当前时间点之后才能执行的延时消息全部移除但当前正在处理的消息以及队列中所有非延时消息会继续执行完毕。然后Looper在消息队列为空后退出。怎么选如果你在HandlerThread里执行的是每条消息都完整、不可丢弃的任务比如写数据库、发网络请求那么千万不要用quit()否则消息丢了你都不知道。如果你只是临时开一个线程做点事做完了立刻走人那quit()也无妨。我强烈建议默认用quitSafely()这个习惯能救你很多次。还有一点Looper的退出机制很独特一旦quit之后再次调用Looper.loop()会直接抛异常。也就是说HandlerThread实例不能被复用。quit之后那个线程就是死的了想继续用只能new一个新的HandlerThread。3.4 getThreadHandler()为什么这个方法能避免空指针public Handler getThreadHandler() { if (mHandler null) { mHandler new Handler(getLooper()); } return mHandler; }这个方法在Android API 28Android 9才加入之前开发者都是自己new Handler(thread.getLooper())。它的价值在于Handler的创建需要Looper而Looper必须初始化完成才能传入否则Handler构造函数会抛异常。getThreadHandler()内部调用了getLooper()所以它天然具备阻塞等待的能力。如果你在线程start之后立刻调用getThreadHandler()它会安全地等待Looper初始化完成再返回Handler。而且这个Handler创建一次之后缓存在mHandler成员变量里。注意Handler是绑定创建它的线程的Looper的不是绑定调用它的线程的——这个Handler后续可以被任意线程调用但消息最终在HandlerThread线程里执行这正好符合串行处理的需求。需要注意一点getThreadHandler()返回的Handler默认只处理普通消息如果你需要处理异步消息设置了消息屏障的场景可以在子类里覆写onLooperPrepared方法自己创建Handler。4. 兄弟方案对比HandlerThread vs 手动ThreadHandler vs 线程池 vs IntentService看完了源码你要真正吃透HandlerThread还需要搞清楚它在整个Android并发工具箱里的位置。我在项目里见过不少滥用线程池、或者是手动管理Thread循环导致内存泄漏的例子根源就是对串行消息循环这个需求的认识不够清晰。4.1 手动Thread Handler的问题生命周期和安全退出难以控制如果不用HandlerThread你自己写一个带消息循环的Thread最常见的实现就是我前面写的那四行代码。但这样做有个致命伤你很难安全地对外暴露Looper更别说在合适时机让循环退出了。线程启动是个异步过程外部线程根本不知道Looper什么时候准备好。如果你在主线程start一个Thread然后立刻调用new Handler(thread.getLooper())大概率会拿到null然后崩掉。要解决只能自己加锁、加CountDownLatch、加标志位这可就费劲了。HandlerThread把这个过程封装好了你直接getLooper()就能阻塞拿到。还有一个问题是线程退出。你自己写的循环如果调用了Looper.loop()那你想退出循环就得调用looper.quit()。但问题是假如你没有保留Looper的引用线程就永远关不掉而且还会持有MessageQueue里的引用导致内存泄漏。HandlerThread把quit()和quitSafely()暴露出来Solution很简单你调用的就是它内部的Looper。4.2 线程池 vs HandlerThread并发模型完全不同别搞混这是很多人在面试里答不清楚的问题。ExecutorService线程池模型是多个线程共同消费一个任务队列它的特征是高吞吐、并发执行你submit()一个任务会有多个工作线程抢着去执行。而HandlerThread模型是一个线程消费自己的消息队列它的特征是串行执行所有消息严格按照先后顺序排队执行不存在并发竞争。所以应该这么选如果你有一系列完全独立、可以并行执行的任务用线程池如果你有一系列有依赖关系、必须顺序执行的任务或者你需要往同一个线程里发延时消息、需要复用同一个线程做定时轮询那就用HandlerThread。我举个例子批量图片上传。每张图片的网络上传都是独立的可以并行用线程池能明显加快速度。但如果你是要把大量日志按顺序写入同一个文件并发写会出错这时候HandlerThread就是最合理的选择——所有写操作排成一队一条一条执行。还有一类场景特别适合HandlerThread串行化不可重入的第三方SDK调用。比如某些蓝牙SDK、串口SDK它们的API并发调用时会出各种竞态问题你只需要把所有调用都post到同一个HandlerThread里就天然串行化了。4.3 IntentServiceHandlerThread的封装弃子IntentService是HandlerThread的一个应用案例它内部创建一个HandlerThread把onHandleIntent重写成消息处理然后在处理完所有任务后自动quitSafely()。但IntentService在API 30已经被标记废弃官方推荐用WorkManager替代。从IntentService身上能看到HandlerThread的经典用法一个服务通过HandlerThread按顺序处理Intent处理完自动退出。后来为什么被废弃因为Android对后台执行的限制越来越严格应用在后台启动Service的机会被压缩得很厉害。这是平台策略问题不是HandlerThread本身的问题——HandlerThread依然活跃在大量非Service场景里。4.4 协程和HandlerThread怎么共存如果你用Kotlin协程可能会问还需要HandlerThread吗协程有自己的Dispatcher比如Dispatchers.IO、Dispatchers.Default它们底层用的是线程池天然支持并行和串行通过单线程Dispatcher。但是我仍然会在下面两种场景用HandlerThread第一种项目里还有老代码它们依赖Handler和Looper来通信。比如一个旧模块回调里有handler.postDelayed()你想让它跑在后台线程上就得有个Looper这时候HandlerThread还是最顺手的。第二种你需要在某些地方精确控制延时取消操作。Handler的消息机制支持removeCallbacks()精确移除某个还没执行的任务协程虽然也有Job取消但两者实现机制不同——在某些场景下Handler的removeCallbacks更直观、更符合业务直觉。结论HandlerThread不是过时产物它是一个简单、稳定、没有魔法黑科技的基础组件。协程能替代它的部分能力但不能替代全部。技术选型不应只看新不新更得看合不合适。5. 实战HandlerThread的典型应用场景与完整实现方案源码看完了方案对比做完了现在进入实战环节。我挑几个真实项目里最常见的用法给你一套可以直接抄走用的代码和注意事项。5.1 场景一串行任务队列保证任务不并发、不丢序这是一个非常典型的需求你有一系列任务需要排队执行前一个没做完后一个绝对不能开始。最常见的实现方式是使用HandlerThread Handler.post(Runnable)。public class SerialTaskExecutor { private final HandlerThread mThread; private final Handler mHandler; public SerialTaskExecutor(String name) { mThread new HandlerThread(name); mThread.start(); mHandler new Handler(mThread.getLooper()); } public void post(Runnable task) { mHandler.post(task); } public void postDelayed(Runnable task, long delayMillis) { mHandler.postDelayed(task, delayMillis); } public void remove(Runnable task) { mHandler.removeCallbacks(task); } public void quit() { mHandler.removeCallbacksAndMessages(null); mThread.quitSafely(); } }注意几个细节Handler的创建必须在HandlerThread的Looper准备好之后否则会崩。这里new Handler(mThread.getLooper())在HandlerThread.start()之后立即调用实际上是安全的因为getLooper()内部有阻塞等待机制。quit()前最好先removeCallbacksAndMessages(null)这个方法会把队列里所有消息都移除掉防止退出后还有消息残留。不过如果调用quitSafely()它会自动处理延时消息这里再调一次是为了保险。如果你想在执行完当前队列里所有任务后自动退出类似IntentService可以维护一个任务计数器在最后一条任务执行完时调用quitSafely()。我在实际项目里用这个方案处理过多个SDK初始化需要按顺序执行的问题。比如应用启动时广告SDK、统计SDK、推送SDK不能并发初始化某些SDK的初始化回调里又依赖上一个SDK的结果。把这些初始化任务全部post到同一个HandlerThread里问题就自然消失了。5.2 场景二本地数据库写入与文件IO的串行化数据库写入和文件IO天然适合串行化。原因是即使你用的是Room或者SQLite如果多个线程同时写同一个数据库Android会抛database is locked异常有时是静默重试但效率很低。用HandlerThread把写操作串行化是最直白的解法。public class LocalDataWriter { private final HandlerThread mThread; private final Handler mHandler; public LocalDataWriter() { mThread new HandlerThread(local-data-writer); mThread.start(); mHandler new Handler(mThread.getLooper()); } public void writeToFile(String path, byte[] data) { mHandler.post(() - { try (FileOutputStream fos new FileOutputStream(path)) { fos.write(data); fos.getFD().sync(); } catch (IOException e) { // 记录错误但不中断队列后续任务 } }); } public void insertToDb(SQLiteDatabase db, ContentValues values) { mHandler.post(() - db.insert(some_table, null, values)); } }这里有一个我踩过的坑如果某个任务执行时间特别长比如写入大文件后面的任务会被严重阻塞。因为HandlerThread是严格串行的一个任务不结束下一个任务永远不能开始。所以千万不要把耗时特别久的任务丢进HandlerThread否则它就成了准单线程。你需要自己评估任务量大不大、单任务耗时多长、是否有可能出现某个任务卡死导致后续所有任务饿死的情况。如果某个任务会在异常分支里无限循环毫无疑问这个HandlerThread就废了——后面的任务全部排队等着主线程如果还再用它做延时回调就跟发生了ANR没什么两样。5.3 场景三延时任务与周期轮询HandlerThreadHandler天生支持延时任务效果和主线程的Handler.postDelayed一样区别只是任务在后台线程执行。这个能力非常适合做轮询服务器状态、定时刷新缓存之类的需求。public class PollingHelper { private final HandlerThread mThread; private final Handler mHandler; private Runnable mPollTask; private static final long POLL_INTERVAL 5000; public PollingHelper() { mThread new HandlerThread(polling-thread); mThread.start(); mHandler new Handler(mThread.getLooper()); } public void startPolling() { mPollTask new Runnable() { Override public void run() { // 执行轮询逻辑比如检查网络、刷新内存缓存 doPoll(); mHandler.postDelayed(this, POLL_INTERVAL); } }; mHandler.post(mPollTask); } public void stopPolling() { mHandler.removeCallbacks(mPollTask); } }这里有一个很关键的细节mHandler.postDelayed(this, POLL_INTERVAL)必须在任务执行完之后再post下一次延时任务这样能保证两次轮询之间的间隔是准确的。如果你用mHandler.post(mPollTask)然后任务内部又post自己可能会造成任务堆积——因为Handler.post是立即加入队列如果任务执行时间超过间隔队列里就会有多个任务排队变成真正的轮询雪崩。所以一定要用postDelayed并且是在任务完成的尾部调用。延时任务还有一个妙用消息屏障和异步消息配合。在Android的MessageQueue中同步消息会被一个屏障消息阻塞只有异步消息能先执行。HandlerThread的Looper也支持这种机制——在某些系统场景比如View绘制、VSYNC调度里主线程会设置消息屏障保证UI消息优先你自己用HandlerThread时也可以这么玩比如优先级高的任务设置异步消息普通任务用同步消息。5.4 场景四配合IdleHandler做空闲时机的延迟操作IdleHandler是MessageQueue里的一个机制当消息队列暂时空闲时会执行IdleHandler里的任务。这个机制配合HandlerThread有一种很有意思的用法——在后台线程空闲时统一处理一些不那么紧急的操作。mThread.getLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() { Override public boolean queueIdle() { // 检测到消息队列空闲在这里做一些低优先级的事 flushPendingMetrics(); // 返回true表示保留这个IdleHandlerfalse表示执行一次后移除 return false; } });比如你在后台线程里有一个缓存预加载队列平时它处理紧急任务当队列空闲时你想趁机多做一点预加载工作就可以用IdleHandler实现。注意IdleHandler返回值的含义返回true表示下次空闲还会执行返回false表示只执行一次。如果你忘了这个坑可能会遇到IdleHandler不会再次执行的情况。需要特别提示IdleHandler在消息队列空闲时才会被调用但如果队列一直有消息在处理IdleHandler可能长时间得不到执行。不要用IdleHandler去做有实时性要求的事它只适合空闲时顺便做点事这种低优先级场景。5.5 处理任务结果如何安全地回到主线程更新UIHandlerThread处理完后台任务后如果结果需要更新UI不能直接在HandlerThread线程里操作View。正确姿势是在任务里通过主线程的Handler把结果post到主线程。public void doAsyncWork(final Callback callback) { mHandler.post(() - { final Object result longRunningOperation(); new Handler(Looper.getMainLooper()).post(() - { if (callback ! null) { callback.onResult(result); } }); }); }如果项目里有RxJava或者协程你可以用更优雅的方式切换线程但原理是一样的——先从HandlerThread切到主线程再更新UI。这里要注意一个生命周期问题如果Activity已经销毁回调不应该继续执行。否则会更新一个不存在的View或者触发状态错乱。常见做法是使用WeakReference引用Activity或者在onDestroy时removeCallbacksAndMessages。6. 我踩过的坑以及排查HandlerThread问题的实战技巧这个章节我原本不想写因为很多是血泪教训。但既然标题叫代码解析只分析原理不分享踩坑过程总感觉少了一半的干货。这里挑几个我真实遇到的线上问题。6.1 致命坑quit之后继续post消息不执行还不报错这个坑非常隐蔽。某次线上反馈某个后台任务一直不执行但log里没有任何异常。我排查了很久最后发现是因为某个业务方在Activity销毁时调用了handlerThread.quit()然后另一个模块还拿着这个Handler继续post任务。消息发出去了吗发出去了。消息进MessageQueue了吗没有——因为MessageQueue在quit之后它的enqueueMessage方法会检查mQuitAllowed如果已经quit新消息不会被加入队列。关键是这一过程不会抛出任何异常也不会返回错误码看起来就像消息被悄无声息地吞掉了。解决方案在HandlerThread旁边维护一个AtomicBoolean状态标记标记是否已quit。每次post前检查状态if (mActive.get()) { mHandler.post(task); } else { // 记录日志或者重新创建HandlerThread }这个检查虽然简单但能极大降低线上问题的排查成本。另一个做法是继承HandlerThread在quit()里标记状态。我推荐组合使用一个状态标记一条检查逻辑。新手最容易翻车的地方是在Activity的onDestroy里调了quit()但其他组件还持有这个Handler的引用。一旦Activity重建旧Handler还活着但线程已经死了后续所有任务全部静默丢失。6.2 糟糕的启动方式线程start之后立即getLooper和post有人会觉得getLooper()里的while循环等一下没有关系所以线程start之后立刻调getLooper()拿到后马上post一个任务。这在绝大多数情况下没问题但是有一个极端情况线程start之后因为线程调度优先级低可能迟迟没有执行run()方法导致getLooper()长时间阻塞。如果你是在主线程调用getLooper()这段时间主线程就会被卡住。我建议在HandlerThread线程内部做初始化比如在onLooperPrepared里创建Handler而不是依赖外部getLooper的等待机制。你可以这样写HandlerThread thread new HandlerThread(my-thread) { Override protected void onLooperPrepared() { mInitializedHandler new Handler(getLooper()); } }; thread.start();然后把mInitializedHandler暴露出去。这样初始化一定发生在子线程主线程不会因为等待Looper而卡顿。或者如果你必须在主线程里创建Handler那就接受getLooper()短暂的阻塞——但要清楚这个阻塞时间取决于线程调度在极端负载下可能远大于预期。6.3 内存泄漏问题HandlerThread持有Activity引用HandlerThread的内部Runnable如果持有了Activity的Context、View、Listener等强引用而线程一直不退就会造成泄漏。特别是在做轮询的时候Runnable里往往持有Activity实例轮询又不停止Activity就永远无法被回收。解决思路很简单任务里不要直接持有Activity强引用改用WeakReference同时在Activity的onDestroy里停止轮询、移除Callbacks、退出线程。Override protected void onDestroy() { super.onDestroy(); mPollingHelper.stopPolling(); mPollingHelper.quit(); }还有一个进阶技巧如果你只是在某个页面暂借HandlerThread处理任务考虑用Lifecycle组件或者Disposable模式管理它的生命周期确保页面销毁时线程自动退出。我在组件化项目里就是这么干的——每个页面有独立的HandlerThread页面销毁就自动quitSafely不会跨页面共享。6.4 性能调优priority参数真的有用吗HandlerThread的构造方法可以传线程优先级比如Process.THREAD_PRIORITY_BACKGROUND。这个优先级是Linux线程的nice值影响的是CPU调度权重。对于低优先级任务比如缓存清理、日志上传、预加载我建议传THREAD_PRIORITY_BACKGROUND这样不至于和主线程抢占CPU时间片。但要注意优先级是相对值不是绝对值。你不能因为传了THREAD_PRIORITY_URGENT_AUDIO就能从低优先级线程直接插队到最前面——Android进程的调度策略很复杂还要考虑进程本身的前后台状态。在实际调试中我觉得priority参数的收益主要是降低后台任务对主线程的影响而不是提升后台任务的速度。所以默认的0THREAD_PRIORITY_DEFAULT没有问题如果你的任务确实非常重要且对延迟敏感那不该用HandlerThread应该直接在主线程或者前台服务里做。另外提一个冷知识HandlerThread的run方法里先调用Process.setThreadPriority(mPriority)之后才调用onLooperPrepared()。这意味着你在onLooperPrepared里做的任何操作都已经应用了目标优先级。如果你觉得某些初始化任务特别重想让它们以更低优先级跑可以在onLooperPrepared里再临时改一次优先级处理完再改回来。6.5 如何确认线程是否退出和排查卡死排查HandlerThread卡死问题我一般用两个手段第一个是线程dump。在终端执行adb shell kill -3 pid可以拿到Java线程的dump信息。找到HandlerThread线程看它的状态——如果它是WAITING或TIMED_WAITING在MessageQueue.next()上说明它在正常等待消息没问题如果它是RUNNABLE状态在一个业务方法里卡住了那就是你的某个消息执行时间过长或者死循环了。第二个是打日志。在关键节点打上enter和exit日志配合时间戳精确判断每条消息的执行耗时。如果你发现某条消息的执行耗时突然暴涨大概率是它内部做了IO或者网络操作。还有一种非常隐蔽的情况HandlerThread不退出Looper.loop()也不退出但MessageQueue里没有任何消息线程处于假死状态。这时候它既不占用太多CPU也不报错但你的任务发进去就是不被执行。通常是你在quitSafely之后又创建了一个新的Handler绑定了旧Looper或者是你把Handler绑定到了另一个线程的Looper上new Handler(其它线程的looper)。排查方式就是打印mThread.getLooper() handler.getLooper()看看Handler到底绑定在哪个Looper上。6.6 HandlerThread的替代方案什么时候考虑HandlerThread虽好但有两个场景我会建议你不要用它第一种是高并发场景。如果你的任务到达速率非常高而且任务本身只耗时极短那么HandlerThread的单线程吞吐量会卡在消息分发和Runnable调度的开销上。这时候用线程池Executor会更合适。第二种是需要协程结构化并发的场景。协程可以通过Dispatchers.IO.limitedParallelism(1)实现串行而且支持取消、超时、组合、异常传播比HandlerThread的消息队列回调模式高级得多。如果新项目已经完全用协程了没必要为了用HandlerThread而用HandlerThread。但就像我之前说的老项目维护、SDK回调兼容、底层机制理解这些场景里HandlerThread依然是绕不开的知识点。尤其是面试的时候HandlerThread的源码解析几乎是必考题——它能带出Looper、Handler、MessageQueue、ThreadLocal、wait/notify、优先级一整串考点。7. 总结一下源码解析中最重要的几个细节HandlerThread的整个实现简洁到只有几个方法但每个方法背后都有值得深挖的设计意图。我建议你重点记住下面这几个关键细节第一getLooper()的阻塞等待机制是HandlerThread的精华。它通过synchronized (this)加while (isAlive() mLooper null)加wait()的经典组合实现了外部安全获取已初始化Looper的能力既不会在Looper未就绪时返回错误结果也不会浪费CPU资源。第二quit()和quitSafely()的区别决定了线上任务的可靠程度。默认用quitSafely()除非你能确保队列里没有必须执行完的任务。一旦quitHandlerThread实例就不可复用想再次使用必须new一个新的。第三onLooperPrepared()是子类扩展的关键切入点和Looper生命周期的重要节点——此时Looper已准备好但loop还没开始所以你可以在这里安全地创建Handler、注册IdleHandler、设置优先级。它在Process.setThreadPriority(mPriority)之后被调用保证了扩展操作已经运行在目标优先级环境下。第四HandlerThread本质是一个线程一个消息循环它的核心能力是串行化和可控生命周期适用于任务有依赖关系、需要精确取消、需要延时执行的后台场景。它的定位和高吞吐的线程池有明显区别。我在实际开发中越来越发现理解这些基础组件并不只是为了面试而是为了在线上问题来临时能快速定位方向。HandlerThread的代码量不大但它连接了Android消息机制的几乎所有核心概念——Looper的创建、Handler的绑定、MessageQueue的调度、线程安全的初始化、退出机制。把这几十行代码读透了你整个Android消息机制的理解深度会上一个台阶。希望这篇文章能帮到你有不同观点也欢迎交流。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →