Handler(三):主线程、HandlerThread、Binder 线程到底怎么区分?

Handler(三):主线程、HandlerThread、Binder 线程到底怎么区分?

前两篇解决的是 Handler 机制本身:

复制代码
第一篇:
    post 之后,Runnable 为什么会在目标线程执行。
​
第二篇:
    Message 入队以后,为什么不是马上执行。

这篇把机制放回 Android 系统线程里。

因为真实阅读源码和排障时,很多问题不是 post() 或 next() 看不懂,而是线程概念混了:

arduino 复制代码
主线程是不是 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?

先放核心结论:

scss 复制代码
主线程:
    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()。

主线是:

scss 复制代码
ActivityThread.main()
  -> Looper.prepareMainLooper()
  -> new ActivityThread()
  -> thread.attach(false, startSeq)
  -> Looper.loop()

源码入口:

这说明主线程不是普通 Thread 加一个名字。它启动时就准备了 MainLooper,并进入 Looper.loop()。

所以:

ini 复制代码
Handler mainHandler = new Handler(Looper.getMainLooper());

这行代码的意义不是"创建主线程",而是:

复制代码
创建一个投递入口;
这个入口持有 MainLooper 引用;
MainLooper 与应用主线程绑定。

主线程天然有 Looper,是因为 Android 在进程启动时替你准备好了。

二、普通线程为什么默认没有 Looper?

普通 Java 线程只负责执行 run()。它不会自动创建消息队列,也不会自动进入循环。

scss 复制代码
new Thread(() -> {
    Looper.myLooper(); // 默认是 null
}).start();

如果你在这种默认没有 Looper 的线程里隐式创建 Handler:

scss 复制代码
new Handler();

构造函数读取到的 Looper.myLooper() 是 null,因此会直接抛出异常。如果线程此前手动执行过 Looper.prepare(),无参构造虽然不会崩溃,却会隐式绑定当前线程的 Looper;无参 Handler 构造被废弃,正是因为这种隐式选择容易造成崩溃、任务丢失、竞态或绑定错误线程。

但"普通线程默认没有自己的 Looper"不等于"普通线程不能创建任何 Handler"。它仍然可以显式持有其他线程的 Looper:

scss 复制代码
new Thread(() -> {
    Handler mainHandler = new Handler(Looper.getMainLooper());
    mainHandler.post(() -> updateUi());
}).start();

这里创建 Handler 的是普通线程,Handler 持有的却是 MainLooper 引用,回调仍由应用主线程执行。真正受限制的是"在没有 Looper 的线程上用无参构造隐式选择当前线程 Looper",不是 new Handler(...) 这个动作本身。

源码入口:

所以普通线程和 Handler 线程的差别不是"是不是子线程",而是:

arduino 复制代码
普通线程:
    执行完 run() 就结束,没有消息循环。
​
带 Looper 的线程:
    prepare 后进入 loop,可以持续接收 Message。

三、HandlerThread 到底是什么?

HandlerThread 不是 Handler。

它是一个带 Looper 的 Thread。

源码主线很短:

scss 复制代码
HandlerThread.run()
  -> mTid = Process.myTid()
  -> Looper.prepare()
  -> mLooper = Looper.myLooper()
  -> notifyAll()
  -> Process.setThreadPriority(mPriority)
  -> onLooperPrepared()
  -> Looper.loop()

源码入口:

把它写成公式就是:

ini 复制代码
HandlerThread = Thread + Looper.prepare() + Looper.loop()

它解决的是:

复制代码
我需要一条可以接收 Handler 消息的工作线程。

而 Handler 解决的是:

复制代码
我需要一个入口,把任务投到某个 Looper 的 MessageQueue。

两者常一起用:

ini 复制代码
HandlerThread thread = new HandlerThread("camera-state");
thread.start();
​
Handler handler = new Handler(thread.getLooper());

这里对象关系是:

arduino 复制代码
thread:
    真正执行任务的线程。
​
thread.getLooper():
    这条线程里的消息循环。
​
handler:
    投递到这条消息循环的入口。

所以不要说"HandlerThread 处理消息"。更准确的是:

复制代码
HandlerThread 里的 Looper 取消息;
消息通过 target 回到 Handler;
最后由 Handler.dispatchMessage 分发回调。

四、HandlerThread 是线程池吗?

不是。

一个 HandlerThread 同一时间仍然只 dispatch 一个 Message。

css 复制代码
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 线程:
    串行处理某个模块的内部状态。

典型链路是:

rust 复制代码
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:

scss 复制代码
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 只是进程名。

你至少要继续问:

arduino 复制代码
是 system_server main 慢?
是 android.ui 慢?
是 android.display 慢?
是 android.io 慢?
是 Binder 线程慢?
还是某个服务自己创建的 HandlerThread 慢?

每个答案对应的排查方向不一样。

如果是 Binder 线程慢:

复制代码
看跨进程调用、调用方等待、Binder 线程池是否耗尽、同步调用链是否过长。

如果是 android.display 慢:

java 复制代码
看窗口、显示、输入相关回调是否耗时,是否有锁等待、同步 Binder、native/HAL 阻塞。

如果是 android.io 慢:

css 复制代码
看 I/O 是否超出预期,是否把本应异步或可取消的工作塞进了串行 Looper。

如果是某个 HandlerThread 慢:

复制代码
看这个线程是否被当成线程池滥用,是否有多个互不相关的耗时任务排队。

所以可定位的说法应该像这样:

arduino 复制代码
目标消息投递到了 system_server 的 android.display Handler;
android.display 当前正在执行前一个窗口回调;
后续显示状态更新在该 Looper 队列中等待。

而不是:

复制代码
system_server 卡了。

八、看到一个 Handler,应该先问哪三件事?

不要先问"它是不是主线程"。

先问三件事:

markdown 复制代码
1. 这个 Handler 持有哪个 Looper?
​
2. 这个 Looper 与哪条 Thread 绑定?
​
3. 这条 Thread 在系统设计里负责什么任务?

这三问能把很多概念混淆拆开。

例子一:

ini 复制代码
Handler handler = new Handler(Looper.getMainLooper());

答案:

复制代码
持有 MainLooper 引用;
MainLooper 与主线程绑定;
这条线程负责 UI 和应用主线回调。

例子二:

ini 复制代码
Handler handler = new Handler(DisplayThread.get().getLooper());

答案:

arduino 复制代码
持有 DisplayThread 的 Looper 引用;
这个 Looper 与 android.display 线程绑定;
这条线程负责显示、窗口、输入相关系统任务。

例子三:

ini 复制代码
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"才有意义。

三篇到这里形成了一条链:

rust 复制代码
任务怎么走:
    Handler.post -> Message -> enqueue -> next -> dispatch。
​
队列怎么等:
    when、同步屏障、nativePollOnce、nativeWake、quit。
​
线程怎么分:
    主线程、HandlerThread、Binder 线程、system_server 专用 Looper。

下一篇进入实战:

复制代码
Handler 卡顿怎么定位?从 Slow delivery 到 Slow dispatch。

延伸阅读

相关推荐
Dovis(誓平步青云)1 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码3 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程6 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓8 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1239 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium