Handler(三):主线程、HandlerThread、Binder 线程到底怎么区分?
前两篇解决的是 Handler 机制本身:
第一篇:
post 之后,Runnable 为什么会在目标线程执行。
第二篇:
Message 入队以后,为什么不是马上执行。
这篇把机制放回 Android 系统线程里。
因为真实阅读源码和排障时,很多问题不是 post() 或 next() 看不懂,而是线程概念混了:
主线程是不是 Handler?
HandlerThread 是不是 Handler?
普通 Thread 中直接 new Handler() 为什么可能崩溃?
Binder 线程和 Handler 线程是什么关系?
system_server 里为什么有 main、android.ui、android.display、android.io?
如果这些边界不清楚,很容易把所有问题都说成:
主线程卡了。
system_server 卡了。
Handler 卡了。
这些说法都太粗。它们没有告诉你目标 Looper 是哪条线程,也没有告诉你这条线程设计上应该做什么。
所以这篇只回答一个问题:
哪些线程长期运行 Looper?
哪些线程负责执行服务端 Binder 入口?
Binder 入口为什么又经常把后续任务交给 Handler?
先放核心结论:
主线程:
Thread + MainLooper。
普通线程:
默认只是 Thread,没有 Looper。
HandlerThread:
Thread + Looper.prepare() + Looper.loop()。
Handler:
持有某个 Looper 引用,是向其 MessageQueue 投递任务和取出后分发消息的入口。
Binder 线程:
从 Binder 驱动接收事务,并在当前线程执行服务端 Binder 入口;
入口代码可以直接处理,也可以把后续状态处理交给 Handler。
system_server 专用线程:
多条带 Looper 的 ServiceThread,用来串行处理不同系统模块的状态。
本文源码以 Android 16 QPR2 的 android-16.0.0_r4 tag 为参考,正文只讨论 Handler 的稳定机制,不展开版本实现差异。
这张图按两个问题读:
第一问:
这条线程是不是在跑 Looper?
第二问:
如果在跑 Looper,它负责哪个系统域的串行状态处理?
这张图只画了 system_server 中长期运行 Looper 的专用线程。Binder 线程池不依靠 Looper.loop() 接收事务,因此没有画进这组 Handler 线程;第五节会单独展开它怎样执行服务端 IPC 入口。
一、主线程为什么天然有 Looper?
应用主线程的入口在 ActivityThread.main()。
主线是:
ActivityThread.main()
-> Looper.prepareMainLooper()
-> new ActivityThread()
-> thread.attach(false, startSeq)
-> Looper.loop()
源码入口:
这说明主线程不是普通 Thread 加一个名字。它启动时就准备了 MainLooper,并进入 Looper.loop()。
所以:
Handler mainHandler = new Handler(Looper.getMainLooper());
这行代码的意义不是“创建主线程”,而是:
创建一个投递入口;
这个入口持有 MainLooper 引用;
MainLooper 与应用主线程绑定。
主线程天然有 Looper,是因为 Android 在进程启动时替你准备好了。
二、普通线程为什么默认没有 Looper?
普通 Java 线程只负责执行 run()。它不会自动创建消息队列,也不会自动进入循环。
new Thread(() -> {
Looper.myLooper(); // 默认是 null
}).start();
如果你在这种默认没有 Looper 的线程里隐式创建 Handler:
new Handler();
构造函数读取到的 Looper.myLooper() 是 null,因此会直接抛出异常。如果线程此前手动执行过 Looper.prepare(),无参构造虽然不会崩溃,却会隐式绑定当前线程的 Looper;无参 Handler 构造被废弃,正是因为这种隐式选择容易造成崩溃、任务丢失、竞态或绑定错误线程。
但“普通线程默认没有自己的 Looper”不等于“普通线程不能创建任何 Handler”。它仍然可以显式持有其他线程的 Looper:
new Thread(() -> {
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(() -> updateUi());
}).start();
这里创建 Handler 的是普通线程,Handler 持有的却是 MainLooper 引用,回调仍由应用主线程执行。真正受限制的是“在没有 Looper 的线程上用无参构造隐式选择当前线程 Looper”,不是 new Handler(...) 这个动作本身。
源码入口:
所以普通线程和 Handler 线程的差别不是“是不是子线程”,而是:
普通线程:
执行完 run() 就结束,没有消息循环。
带 Looper 的线程:
prepare 后进入 loop,可以持续接收 Message。
三、HandlerThread 到底是什么?
HandlerThread 不是 Handler。
它是一个带 Looper 的 Thread。
源码主线很短:
HandlerThread.run()
-> mTid = Process.myTid()
-> Looper.prepare()
-> mLooper = Looper.myLooper()
-> notifyAll()
-> Process.setThreadPriority(mPriority)
-> onLooperPrepared()
-> Looper.loop()
源码入口:
把它写成公式就是:
HandlerThread = Thread + Looper.prepare() + Looper.loop()
它解决的是:
我需要一条可以接收 Handler 消息的工作线程。
而 Handler 解决的是:
我需要一个入口,把任务投到某个 Looper 的 MessageQueue。
两者常一起用:
HandlerThread thread = new HandlerThread("camera-state");
thread.start();
Handler handler = new Handler(thread.getLooper());
这里对象关系是:
thread:
真正执行任务的线程。
thread.getLooper():
这条线程里的消息循环。
handler:
投递到这条消息循环的入口。
所以不要说“HandlerThread 处理消息”。更准确的是:
HandlerThread 里的 Looper 取消息;
消息通过 target 回到 Handler;
最后由 Handler.dispatchMessage 分发回调。
四、HandlerThread 是线程池吗?
不是。
一个 HandlerThread 同一时间仍然只 dispatch 一个 Message。
Message A 正在执行:
Message B 只能等。
Message A 做了慢 I/O:
后面所有 Message 都被拖住。
Message A 等锁:
这条 HandlerThread 上的串行状态机也一起停住。
这就是 HandlerThread 最容易被误用的地方。它适合承载需要串行化的状态处理,比如:
设备状态机;
显示状态变更;
权限状态同步;
某个模块内部必须按顺序处理的事件。
它不适合承载大量互不相关的耗时任务。
如果任务彼此独立、可以并发、主要是计算或 I/O,应该考虑:
Executor;
ExecutorService;
线程池;
协程;
专门的异步框架。
HandlerThread 的官方注释也建议:如果不是必须和 Handler API 对接,优先考虑 Executor、ExecutorService 或 Kotlin coroutine。
所以判断一个 HandlerThread 是否合适,要问:
这些任务是否必须在同一条线程按顺序执行?
这条线程是否允许被某个任务长时间占住?
后续消息被排队等待时,系统表现是否可接受?
五、Binder 线程是 Handler 线程吗?
不是。
Binder 线程和 Handler 线程解决的是两类问题。
Binder 线程:
执行服务端 Binder 入口代码。
Handler 线程:
串行处理某个模块的内部状态。
典型链路是:
App 进程
-> Binder transact
-> Binder 驱动把事务交给 system_server 的 Binder 池线程
-> Binder.execTransact() / Binder.onTransact()
-> AIDL Stub 分发到具体服务方法
-> 服务端入口做参数校验、权限检查、身份处理
-> 入口代码选择:
直接在 Binder 线程完成处理;
post 到 Handler 后返回;
post 到 Handler 后同步等待结果;
或继续同步调用其他 Binder 服务。
源码入口:
Handler 不是 Binder 调用的必经下一层。只有当服务端入口选择把后续工作转给 Handler 时,才需要继续区分两个关键分叉:
服务内部异步投递模式:
Binder 线程把任务投递到 Handler 后很快返回;
App 通过后续回调、状态变化、广播或下一次查询看到结果。
服务内部同步等待模式:
Binder 线程把任务投递到 Handler 后,还要等待 Handler 线程处理结果;
如果 Handler 线程积压或阻塞,Binder 线程也会被拖住;
App 看到的就是同步 Binder 调用变慢。
这里的“异步投递 / 同步等待”描述的是服务端入口是否等待 Handler 结果,不等同于 AIDL oneway 与普通同步 Binder 事务的区分。
这个分叉很重要。看到“服务内部 post 到 Handler”,不能立刻认为 Binder 线程已经完全脱身。要继续看它是投递后返回,还是投递后同步等待结果。
为什么不在 Binder 线程里把所有业务做完?
因为 Binder 线程池是共享资源。如果一个 Binder 线程长时间处理业务逻辑、等待锁、做 I/O 或同步调用别的服务,就会拖住调用方,也会消耗 system_server 的 Binder 线程池容量。
很多系统服务会把真正的状态变更投递到自己的 Handler 线程,是为了获得几个好处:
串行化:
同一个模块的状态变更按顺序处理,减少锁复杂度。
服务内部异步投递时解耦调用方:
Binder 线程在投递被接受后尽快返回,耗时处理留给目标 Handler 线程。
隔离职责:
Binder 线程执行 IPC 入口,Handler 线程处理需要串行化的模块状态。
便于排障:
可以具体定位到某条 Looper 线程,而不是笼统说 system_server 慢。
服务内部同步等待模式只能保留“业务状态由目标线程串行处理”这个好处,不能说已经解耦调用方。Binder 线程仍在等待结果,目标 Handler 的排队、锁等待和慢回调会沿同步调用链传播给 App。
但这不代表“post 到 Handler 就一定没问题”。如果 Handler 线程自己积压或阻塞,问题只是换了位置:
异步投递模式下,Binder 入口很快把消息投出并返回;
但 Handler 所在线程一直没处理;
用户看到的效果仍然迟迟不发生。
这就是为什么后续排障必须同时看 Binder 线程和目标 Handler 线程。
六、system_server 为什么不只有一条主线程?
system_server 是进程,不是一条线程。
它里面有主线程,也有 Binder 线程池,还有多条专用 Handler 线程。
system_server 主线程也会准备 MainLooper:
SystemServer.run()
-> Looper.prepareMainLooper()
-> startBootstrapServices()
-> startCoreServices()
-> startOtherServices()
-> Looper.loop()
源码入口:
但很多系统任务不会全部压在主线程上。Framework 里有多条专用线程,常见的包括:
| 线程 | 本质 | 常见职责 |
|---|---|---|
system_server main | system_server 主 Looper | 服务启动、部分系统服务主线任务 |
android.ui | system_server 内部 UI 相关 ServiceThread | 需要避开系统主线程的短时 UI 相关工作 |
android.display | 显示相关 ServiceThread | DisplayManager、WindowManager、InputManager 等显示/窗口/输入相关任务 |
android.anim | 动画相关 ServiceThread | 窗口动画、和动画时序相关任务 |
android.io | I/O 相关 ServiceThread | 允许短暂阻塞的系统 I/O 任务 |
android.perm | 权限相关 ServiceThread | permission、appops 等同步状态 |
| Binder 线程 | Binder 线程池工作线程 | 执行服务端 Binder 入口;入口可以直接处理或选择转交任务 |
这里的 android.ui 是 system_server 进程内部的 UiThread,不是 com.android.systemui 应用进程的 main 线程。两者进程、Looper 和职责都不同,排障时不能混为一谈。
源码入口:
这些线程名字不是装饰。它们决定系统服务把某些任务投到哪里串行处理。
例如窗口、显示、输入相关问题,不能只盯着 App 主线程或 SurfaceFlinger。很多状态变更会先进入 system_server,再被投到 android.display 或相关 Handler 线程处理。
七、排障时为什么不能只说 system_server 慢?
因为 system_server 只是进程名。
你至少要继续问:
是 system_server main 慢?
是 android.ui 慢?
是 android.display 慢?
是 android.io 慢?
是 Binder 线程慢?
还是某个服务自己创建的 HandlerThread 慢?
每个答案对应的排查方向不一样。
如果是 Binder 线程慢:
看跨进程调用、调用方等待、Binder 线程池是否耗尽、同步调用链是否过长。
如果是 android.display 慢:
看窗口、显示、输入相关回调是否耗时,是否有锁等待、同步 Binder、native/HAL 阻塞。
如果是 android.io 慢:
看 I/O 是否超出预期,是否把本应异步或可取消的工作塞进了串行 Looper。
如果是某个 HandlerThread 慢:
看这个线程是否被当成线程池滥用,是否有多个互不相关的耗时任务排队。
所以可定位的说法应该像这样:
目标消息投递到了 system_server 的 android.display Handler;
android.display 当前正在执行前一个窗口回调;
后续显示状态更新在该 Looper 队列中等待。
而不是:
system_server 卡了。
八、看到一个 Handler,应该先问哪三件事?
不要先问“它是不是主线程”。
先问三件事:
1. 这个 Handler 持有哪个 Looper?
2. 这个 Looper 与哪条 Thread 绑定?
3. 这条 Thread 在系统设计里负责什么任务?
这三问能把很多概念混淆拆开。
例子一:
Handler handler = new Handler(Looper.getMainLooper());
答案:
持有 MainLooper 引用;
MainLooper 与主线程绑定;
这条线程负责 UI 和应用主线回调。
例子二:
Handler handler = new Handler(DisplayThread.get().getLooper());
答案:
持有 DisplayThread 的 Looper 引用;
这个 Looper 与 android.display 线程绑定;
这条线程负责显示、窗口、输入相关系统任务。
例子三:
Handler handler = new Handler(handlerThread.getLooper());
答案:
持有某个自建 HandlerThread 的 Looper 引用;
这个 Looper 与这条自建线程绑定;
职责取决于创建它的模块。
这才是 Handler 排障的入口。
九、核心区分表
把这一篇压缩成一张表:
| 线程类型 | 本质 | 常见误解 | 准确说法 |
|---|---|---|---|
| 主线程 | Thread + MainLooper | Handler 就是主线程 | 主线程天然有 MainLooper,Handler 是持有它的投递与分发入口 |
| 普通线程 | Thread | 默认能用无参 Handler 绑定自己 | 默认没有自己的 Looper,但仍可显式绑定其他线程的 Looper |
| HandlerThread | Thread + Looper | 它是 Handler 或线程池 | 它是带 Looper 的线程,同一时间仍然只 dispatch 一个消息 |
| Binder 线程 | Binder 线程池工作线程 | 它只是接收或转发调用 | 它会在当前线程执行服务端 Binder 入口;入口可直接处理,也可选择转交 Handler |
| system_server main | system_server 主 Looper | system_server 只有这一条线 | system_server 还有 Binder 线程和多条专用 ServiceThread |
android.ui | system_server 内部 UI Handler 线程 | 它就是 SystemUI 进程主线程 | 它是 system_server 的 UiThread,与 com.android.systemui 的 main 线程不同 |
android.display | 显示/窗口/输入相关 Handler 线程 | display 问题都在 SurfaceFlinger | Framework 显示状态也可能在 system_server 线程 |
android.io | 系统 I/O Handler 线程 | 后台线程可以无限阻塞 | 它仍是串行 Looper,阻塞会造成后续消息积压 |
十、这一篇的结论
Handler 排障首先是线程定位。
看到一个 Handler,不要先说“它是主线程”或“它是子线程”。应该先问:
这个 Handler 持有哪个 Looper?
这个 Looper 与哪条 Thread 绑定?
这条 Thread 在系统设计里负责什么任务?
只有线程定位清楚,后面讨论“消息为什么没执行”“是不是 slow delivery”“是不是 slow dispatch”才有意义。
三篇到这里形成了一条链:
任务怎么走:
Handler.post -> Message -> enqueue -> next -> dispatch。
队列怎么等:
when、同步屏障、nativePollOnce、nativeWake、quit。
线程怎么分:
主线程、HandlerThread、Binder 线程、system_server 专用 Looper。
下一篇进入实战:
Handler 卡顿怎么定位?从 Slow delivery 到 Slow dispatch。