尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android Handler 机制全解析:从消息循环到同步屏障与 ANR

Android Handler 机制全解析:从消息循环到同步屏障与 ANR 在 Android 中点击事件、绘制、生命周期回调最终都要在相应线程上执行。Handler经常被一句话概括为“切回主线程的工具”但它真正依托的是一套线程内的消息循环。理解这套机制才能回答postDelayed为什么不是准时执行、主线程为什么不会因为Looper.loop()而卡死、同步屏障为何能让部分消息“插队”以及 Handler 内存泄漏究竟是谁持有了谁。本文以 Android SDK 的公开行为为主源码流程使用简化伪代码说明涉及MessageQueue、ViewRootImpl等内部实现时会明确其不是稳定的公开 API。一、先看全貌四个角色一条链路角色职责关键关系Message携带what、obj、时间、Runnable等任务信息发送后会关联目标HandlerHandler把任务送入队列并在目标线程分发它构造时绑定一个LooperMessageQueue按执行时间管理待处理消息并在无可执行任务时等待每个Looper持有一个队列Looper不断从队列取消息交给目标Handler执行通常一个线程最多准备一个Looper工作线程 / 其他调用方 │ handler.post(runnable) / sendMessage(message) ▼ 目标线程的 MessageQueue ── 按 when 排队无就绪消息时等待 │ next() 取出到期消息 ▼ 目标线程的 Looper.loop() │ msg.target.dispatchMessage(msg) ▼ Runnable / Handler.Callback / handleMessage() │ └─ 分发结束后回收 Message最重要的一点**Handler.post()不创建线程代码在哪个线程执行取决于 Handler 绑定的 Looper 属于哪个线程。**同一个 Looper 可以有多个 Handler它们共用该线程与消息队列。二、从一个最小例子开始privatevalmainHandlerHandler(Looper.getMainLooper())funloadPreview(){backgroundExecutor.execute{valpreviewdecodePreview()// 耗时工作在后台线程mainHandler.post{previewView.setImageBitmap(preview)// 在主线程更新 UI}}}Handler(Looper.getMainLooper())明确绑定主线程。后台线程调用post只是把Runnable入主线程队列post返回并不代表 UI 已更新。主线程取到消息后才会执行它如果前面有耗时任务它还要等。不要用无参的Handler()猜测“当前线程是谁”这个隐式绑定构造方式已被弃用也可能在没有 Looper 的线程上失败。需要主线程就写Looper.getMainLooper()需要工作线程则显式使用其Looper。post和sendMessage有何区别post(runnable)会把Runnable放进消息的callback字段sendMessage(message)更适合根据what或数据做统一分发。两者最终都走同一条队列与dispatchMessage链路。privateconstvalMSG_REFRESH1valhandlerobject:Handler(Looper.getMainLooper()){overridefunhandleMessage(msg:Message){if(msg.whatMSG_REFRESH)refreshUi()}}handler.sendEmptyMessage(MSG_REFRESH)新代码通常更容易用post {}表达一次性工作what消息适用于确实需要按类型集中处理的场景。不要把大量业务数据塞进Message.obj后靠强制类型转换维护协议。三、Looper 是怎样和线程绑定的普通工作线程默认没有消息循环。概念上手动创建 Looper 的流程是valthreadThread{Looper.prepare()// 为当前线程创建 Looper 和 MessageQueuevalhandlerHandler(Looper.myLooper()!!)publishHandler(handler)// 示例伪代码安全地交给其他线程Looper.loop()// 开始取消息通常直到 quit}thread.start()这里的publishHandler只是说明必须考虑初始化同步不是 Android API。Looper.prepare()把 Looper 存入当前线程的ThreadLocal同一线程重复调用会报错。Looper.myLooper()只查当前线程Looper.getMainLooper()则获取主线程 Looper。应用主线程的 Looper 由系统在应用进程启动时准备并运行开发者不应在主线程再次调用prepare()或loop()。业务代码若需要长期运行的工作消息循环可用HandlerThread多数普通后台任务更适合线程池、协程或其他有明确生命周期的执行器。HandlerThread的使用边界valworkerThreadHandlerThread(sensor-serial).apply{start()}valworkerHandlerHandler(workerThread.looper)workerHandler.post{processSensorBatch()}// 组件不再需要这个专有线程时workerThread.quitSafely()HandlerThread是带 Looper 的线程适合需要串行处理、线程亲和性的工作。quitSafely()会处理已到期的待处理消息未来才到期的延迟消息不会保证执行已经开始运行的回调也不会被强制中断。线程退出后不应继续向它的 Handler 投递任务。若任务需要并行、等待网络或跨进程持久执行HandlerThread不是默认选择。四、一条消息从发送到执行经历了什么以handler.postDelayed(task, 300)为例可把内部流程理解为六步postDelayed把Runnable包装进Message并计算基于SystemClock.uptimeMillis()的目标执行时间。Handler把自己设为消息的target调用队列的入队逻辑。MessageQueue按when维护消息顺序如果新消息需要更早执行会唤醒可能正在等待的 Looper。Looper.loop()反复调用队列的next()没有到期消息时底层可进入等待而不是持续空转。取到消息后在Looper 所在线程调用msg.target.dispatchMessage(msg)。分发结束后Looper 将消息放回消息池供复用发送后的Message不应再被调用方修改、保留或自行重复回收。在常见 AOSP 实现里队列以按时间链接的消息节点组织待办工作next()计算距离下一条到期消息的等待时间再通过原生层的nativePollOnce等待事件或超时。若新消息需要更早处理入队侧会通过nativeWake唤醒它。这些函数和内部数据结构用于解释“为什么不会忙等”不属于应用应依赖的稳定接口。分发顺序可以概括为msg.callbackpost 的 Runnable存在 → 运行 Runnable 否则 Handler.Callback.handleMessage(msg) 返回 true → 已消费 否则 → Handler.handleMessage(msg)Handler.Callback与子类重写的handleMessage并不是无条件都执行前者返回true就不会再走后者。为什么Looper.loop()不会让主线程一直“忙等”“无限循环”只意味着主线程不断接收任务不意味着 CPU 不断执行空循环。队列没有到期消息时会等待底层事件新消息入队或等待时间到达后再被唤醒。真正拖慢主线程的是被分发的回调执行太久或过多任务持续占满队列。postDelayed(300)是 300 毫秒后准时执行吗不是。它表示最早满足延迟条件后才有资格执行还必须等前面的工作结束设备深度休眠期间uptimeMillis()不计时也会影响实际墙上时间。Handler 不是精确计时器更不能保证进程退出后任务仍执行。需要可持久的延期工作应选 WorkManager 等合适机制。sendMessageAtFrontOfQueue()可以把消息放到队首但滥用会饿死普通消息它也无法打断当前正在运行的回调。五、同步屏障和异步消息为什么有消息能越过队头正常情况下队列按时间选择消息。Android 内部还存在同步屏障队列中放入一个没有目标 Handler 的特殊节点。屏障生效时其后的普通同步消息暂时不能通过但异步消息可以被选出执行。队列顺序同步消息 A → [同步屏障] → 同步消息 B → 异步消息 C 屏障前A 正常执行 屏障后B 暂缓C 可以越过屏障执行 屏障移除B 才继续获得执行机会上图是概念示意不表示所有消息一定按图中的时间先后入队。Android 的视图遍历调度会在内部使用同步屏障把绘制相关工作安排在合适阶段。同步屏障的插入和移除不是应用可依赖的公开接口不要通过反射自己操作它。从 API 28 起可以使用Handler.createAsync(looper)创建异步 Handler它发出的消息不会被同步屏障挡住但仍在同一个线程执行不会并行、不会抢占正在运行的任务也不是解决主线程卡顿的捷径。异步消息与普通同步消息之间的相对顺序不应被当作业务保证。六、IdleHandler队列空闲时能做什么MessageQueue.IdleHandler可以在队列没有就绪消息、即将等待时收到回调队列里有未来才到期的消息也可能处于这种“当前空闲”状态。queueIdle()返回true表示保留以后继续调用返回false表示移除。它运行在 Looper 所在线程。如果加在主线程队列就不能在里面做大量计算、磁盘读写或等待网络“空闲时调用”不等于“有无限空闲时间”。业务上更稳妥的方式是把真正的耗时工作交给后台执行器并把可见结果送回主线程。七、Handler 的内存泄漏到底怎么发生常见的引用链不是“Handler 本身神秘地泄漏”而是队列里的未执行任务延长了对象寿命主线程 MessageQueue └─ 延迟 Message └─ Runnable / Handler └─ Activity、View 或大对象例如匿名内部类、lambda 捕获了 Activity消息又延迟十分钟。Activity 销毁后队列仍持有任务直到任务执行或被移除。把 Handler 改成静态类只能切断一部分引用Runnable继续捕获页面对象仍可能保留它。privatevalmainHandlerHandler(Looper.getMainLooper())privatevalrefreshTaskRunnable{refreshUi()}funscheduleRefresh(){mainHandler.postDelayed(refreshTask,10_000)}overridefunonDestroy(){mainHandler.removeCallbacks(refreshTask)super.onDestroy()}上例中 Handler 和任务由当前组件独占。若 Handler 是共享的不要随手removeCallbacksAndMessages(null)否则可能误删别人的工作。removeCallbacks、hasCallbacks等需要扫描队列复杂度随队列长度增长大量任务更适合用独立取消标记、任务所有权或生命周期感知 API 设计。弱引用也不是万能修复仍需定义任务何时取消、结果过期后如何处理。八、Handler 与 ANR该查的是哪段回调ANR 的关键不是“队列里消息多”这个数字而是主线程在关键时刻不能及时响应。一段主线程回调进行同步网络请求、数据库查询、锁等待或大规模计算后面的输入、绘制及生命周期工作就只能排队。即使每个小任务都不长持续高频投递也可能让主线程长期繁忙。排查时可按下面的顺序从 ANR traces、Perfetto 或 Android Studio System Trace 看主线程当时在运行什么还是在等锁、Binder 或 I/O。记录关键消息分发耗时Looper.setMessageLogging可辅助调试但不要在生产环境无节制打印每条消息。检查是否有循环post、高频postDelayed、过期任务未取消或把重活放进了handleMessage。将耗时工作移出主线程同时控制后台并发不要只是把所有工作转移到另一个单线程 Handler 里继续排长队。调用Handler.post本身成功只表示任务被接受入队不证明它在限定时间内完成。post/sendMessage也可能返回false例如目标 Looper 正在退出需要交付保证的业务不能只依赖内存中的消息队列。九、Handler、协程和 WorkManager 怎么选需求更常用的选择理由已有后台结果简单切回主线程主线程Handler.post或Dispatchers.Main轻量地安排 UI 线程工作页面内异步任务需随页面取消viewModelScope、lifecycleScope等协程作用域生命周期与取消关系更明确长期串行、线程亲和的回调处理HandlerThread/ 专用 Looper同一线程依次处理消息多个独立后台任务有界线程池或协程调度器能控制并发不受单个 Looper 串行限制离开页面或进程后仍需完成的延期工作WorkManager 等系统调度方案可持久化与重试普通 Handler 消息不具备这些能力协程没有“取代”底层消息循环。Dispatchers.Main在 Android 上仍需要把恢复工作安排到主线程执行。选型的重点是业务语义是否需要结果、取消、持久性、并行性和生命周期所有权。十、面试高频追问速答**一个线程能有几个 Looper一个 Looper 能有几个 Handler**通常一个线程最多准备一个 Looper一个 Looper 可以对应多个 Handler它们共享同一条线程和队列。**Handler 一定要在主线程创建吗**不必。创建时显式传入哪个 Looper它就向那个 Looper 所在线程分发跨线程调用post没问题。**主线程 Looper 死循环为何不是 ANR**无消息时底层等待不会忙等ANR 往往是某个回调、锁或 I/O 阻塞了主线程的及时响应。**post的任务和sendMessage的消息分发顺序**两者都进入消息队列主要由计划执行时间与队列机制决定post任务优先走msg.callback。**postDelayed用的是什么时间**基于uptimeMillis()不是墙上时钟只是最早可执行时间不是准时保证。**Handler(Looper.getMainLooper())能做耗时工作吗**不能。它把工作安排到主线程耗时逻辑仍会阻塞 UI。**为什么Handler()不推荐**它隐式绑定当前线程 Looper容易在无 Looper 的线程失败或绑定到意料之外的线程无参构造已弃用。**HandlerThread和线程池有何区别**前者是一条带消息循环的专用线程任务串行线程池可以由多个工作线程处理任务适合有界并发。**quit()与quitSafely()区别**前者直接终止消息循环并丢弃未处理消息后者先处理已到期消息未来延迟消息不会继续等待执行。**同步屏障挡住什么**挡住屏障后的同步消息异步消息可越过屏障。它不抢占当前回调也不是公开的业务调度 API。**为什么静态内部类 Handler 仍可能泄漏**队列中的Runnable或Message.obj仍可能持有 Activity需要检查整条引用链与取消时机。**如何判断消息“丢了”**先看投递返回值、Looper 是否退出、任务是否被移除、延迟是否到期以及主线程是否被前面的回调堵住不能把“还没执行”直接当作丢失。最后把 Handler 机制记成一句话**调用方把任务交给绑定目标 Looper 的 Handler队列按规则保存与等待目标线程的 Looper 取出消息并回调 Handler。**性能与内存问题也沿这条链查任务什么时候入队、谁持有它、什么时候被执行或取消。参考资料Android APIHandlerAndroid APILooperAndroid APIMessageQueueAndroid APIMessageAndroid APIHandlerThreadAndroid APISystemClockAndroid 开源代码ViewRootImpl
返回列表