Handler 运行流程源码解析(五):quit() & quitSafely()

文章内容

本文根据视频《子线程中维护的 Looper 在无消息的时候应该怎么处理减少性能问题》整理。

子线程调用 Looper.loop() 后,即使没有消息,方法也不会自动返回。看到这里的无限循环,很容易产生两个疑问:它会不会一直占用 CPU?如果不再使用这条线程,又该怎样让它结束?

这两个问题需要分开回答:暂时没有消息时,Looper 会阻塞等待,不需要额外加休眠;业务不再需要这条消息线程时,应主动调用 quit() 或 quitSafely(),让循环有机会结束。 空闲是正常状态,超过业务生命周期仍然存活,才是需要处理的资源滞留问题。

一、没有消息时,线程究竟停在哪里

子线程消息循环卡在无消息处

子线程建立消息循环的基本顺序是:

java
Looper.prepare();Handler handler = new Handler(Looper.myLooper());Looper.loop();// loop() 正常返回后,才会执行后续代码。

prepare() 为当前线程准备 Looper 和 MessageQueue;Handler 绑定这个 Looper;loop() 则不断取出消息,交给对应的 Handler 分发。处理回调运行在哪条线程上,由 Handler 绑定的 Looper 决定。

在 Android 14 中,单轮取消息和分发的逻辑位于 Looper.loopOnce(),外层 loop() 重复调用它。把两层合并后,核心控制流可以表示为:

java
// 控制流示意,省略消息回收、异常及监控等逻辑。for (;;) {    Message msg = queue.next();    if (msg == null) {        return;    }    msg.target.dispatchMessage(msg);}

关键在 queue.next():它不是“没有消息就立即返回”的查询方法,而是会等待下一条可执行消息。因此,外层虽然是无限循环,却不会在空队列上持续空转。

MessageQueue.next() 通过 nativePollOnce(ptr, nextPollTimeoutMillis) 进入 native 轮询。超时参数有三种典型取值:

参数 含义
0 不阻塞,先检查一次
正数 最多等待指定毫秒数,常用于等待延迟消息到期
-1 没有可确定的下一次消息执行时间,等待唤醒事件

队列为空、完成必要的空闲处理后,线程可以进入无限期等待。这里的“无限期”指没有设置超时时间,并不表示无法醒来:新消息入队时,队列会根据需要唤醒它;请求退出也会触发唤醒。

二、空闲等待为什么仍然需要管理

阻塞等待期间,线程不会因为 Java 层的无限循环持续消耗 CPU,但不能据此断言整条线程的 CPU 占用永远为零。消息处理、唤醒、空闲回调以及其他监听事件仍然可能让它运行。

空闲线程也仍然占用资源:线程栈和线程管理结构没有消失,Looper、MessageQueue 及相关 native 资源仍然存在。线程局部变量、待执行消息和回调还可能保留业务对象的引用。具体内存占用取决于运行环境,不能简单归结为固定的栈大小。

如果这条线程还要持续接收任务,等待正是预期行为。相反,如果页面或任务已经结束,却每次进入页面都再创建一条永不退出的消息线程,线程数量和引用关系就会不断累积。

因此,不能把“队列当前为空”当成销毁条件。应由拥有这条线程的组件判断:以后是否还会向它提交工作? 若还会使用,就保留等待;若使用已经结束,就停止投递并请求退出。

三、quit 与 quitSafely 改变了什么

MessageQueue.quit(safe) 做三件事

Looper 暴露两个退出入口,它们把请求转交给 MessageQueue:

java
public void quit() {    mQueue.quit(false);} public void quitSafely() {    mQueue.quit(true);}

MessageQueue.quit(boolean safe) 的核心逻辑如下:

java
void quit(boolean safe) {    if (!mQuitAllowed) {        throw new IllegalStateException("Main thread not allowed to quit.");    }    synchronized (this) {        if (mQuitting) {            return;        }        mQuitting = true;        if (safe) {            removeAllFutureMessagesLocked();        } else {            removeAllMessagesLocked();        }        nativeWake(mPtr);    }}

这里的执行顺序是:先标记 mQuitting = true,再按策略移除消息,最后发出 native 唤醒信号。状态更新和消息清理使用同一把锁,next() 随后在锁内检查时,能看到完成后的队列状态。

退出标志和唤醒各有作用。标志告诉队列“不再继续接收工作”;唤醒让正在等待的线程及时回来检查这个状态。只设置标志,阻塞中的线程可能没有机会读取它;只唤醒而没有退出状态,线程仍会按正常流程取消息或继续等待。

进入退出状态后,新消息的入队请求会失败,Handler.sendMessage()、post() 等方法会返回 false。重复请求退出会直接返回;这也意味着,先调用 quitSafely() 后再调用 quit(),不能靠第二次调用改变已经选择的清理策略。上述行为见 AOSP MessageQueue。

四、“安全退出”保留哪些消息

quit 与 quitSafely 的差别及主线程不可退出

两个方法都不会中断已经开始执行的回调,区别在于如何处理仍留在队列中的消息。

消息状态 quit() quitSafely()
已取出、正在执行 不主动中断 不主动中断
队列中已到期、可正常派发 移除 保留并继续派发
队列中尚未到期 移除 移除
退出请求生效后新投递 拒绝入队 拒绝入队

quitSafely() 从 API 18 起提供。它的“安全”指处理已经到期的待执行消息,不承诺等待所有延迟任务,也不表示调用方会等待线程结束。这些 API 约定见 Looper 官方文档。

在本文源码版本中,安全退出通过 SystemClock.uptimeMillis() 取得清理时刻:when 大于该时刻的消息属于未来消息,会被移除。例如,队列中有两条已经到期的普通消息和一条一分钟后执行的延迟消息,安全退出会保留前两条,丢弃第三条。

对应的清理方法不需要等待未来时间到来。removeAllFutureMessagesLocked() 找到第一条未来消息,切断链表并回收后半段;removeAllMessagesLocked() 则遍历清理整条链表。消息回收会清除其持有的业务引用,符合条件的 Message 对象可以进入对象池复用。释放引用不等于相关对象立刻被 GC,也不等于进程内存立即下降。

这里讨论的是没有同步屏障的普通工作队列。同步屏障会影响消息是否可派发,不能把“到期消息全部执行”无条件套到存在屏障的内部队列上。

五、已经标记退出,为什么还能继续处理消息

Looper 退出生效链条

理解 quitSafely() 的关键,是 next() 内两个判断的先后顺序:先尝试返回可执行消息,再检查退出标志。 以下示意保留了这个顺序:

java
// 普通无屏障队列的控制流示意。Message next() {    final long ptr = mPtr;    if (ptr == 0) {        return null;    }    int timeoutMillis = 0;    for (;;) {        nativePollOnce(ptr, timeoutMillis);        synchronized (this) {            long now = SystemClock.uptimeMillis();            Message msg = mMessages;            if (msg != null && msg.when <= now) {                mMessages = msg.next;                msg.next = null;                msg.markInUse();                return msg;            }            if (mQuitting) {                dispose();                return null;            }            timeoutMillis = msg == null                    ? -1                    : (int) Math.min(msg.when - now, Integer.MAX_VALUE);        }    }}

安全退出后,保留下来的到期消息仍然可以从前面的分支返回,随后被 Handler 分发。当没有可返回的消息时,next() 才走到 mQuitting 分支,调用 dispose() 释放队列持有的 native 资源并返回 null。

完整的正常退出流程是:

  1. 调用 quitSafely(),队列进入退出状态并移除未来消息。
  2. nativeWake() 发出唤醒信号,使阻塞的轮询有机会返回。
  3. 工作线程继续处理保留下来的、可正常派发的到期消息。
  4. next() 检测到退出状态,清理并返回 null。
  5. Looper.loop() 返回,继续执行其后的代码。
  6. 所属线程的 run() 执行完毕,线程才真正结束。

如果调用退出时线程正在执行回调,退出请求不会把它从回调里拉出来。回调若一直不返回,循环也无法进入下一轮完成退出。Thread.interrupt() 不能代替 Looper 的退出协议;业务回调需要取消时,应另行设计协作式取消逻辑。

退出也可以由工作线程自己的消息回调发起。不能把唯一的退出调用放在 Looper.loop() 后面,因为正常情况下必须先有人请求退出,后面的代码才有机会执行。

六、在业务代码中落实生命周期

子线程三步走(再加一步释放)

手写 prepare()、创建 Handler 和 loop() 适合理解原理。实际需要一条串行消息线程时,可以用 HandlerThread 封装这些步骤,但它仍然需要明确的关闭时机。

下面的示例要求启动、投递和关闭方法均由主线程调用,并假设 API 18 及以上:

java
private HandlerThread workerThread;private Handler workerHandler; // 由主线程调用。private void startWorker() {    if (workerThread != null) {        return;    }    workerThread = new HandlerThread("message-worker");    workerThread.start();    workerHandler = new Handler(workerThread.getLooper());} // 由主线程调用;false 表示当前无法接收任务。private boolean submitWork(Runnable task) {    return workerHandler != null && workerHandler.post(task);} // 由主线程在所属业务生命周期结束时调用。private void closeWorker() {    HandlerThread thread = workerThread;    workerHandler = null;    workerThread = null;    if (thread != null) {        thread.quitSafely();    }}

getLooper() 会等待已启动线程完成 Looper 初始化,避免手写示例中“主线程已经开始投递,子线程的 Handler 却尚未创建”的竞态。重复启动检查也避免了每次操作都额外创建一条消息线程。

closeWorker() 清除投递入口并请求退出,但旧线程仍可能在处理保留的任务。若立即重新启动,旧线程和新线程可能短暂并存。业务若要求前一条线程完全结束后才能重建,应由后台协调代码等待旧线程 join(),不要在主线程上无界等待。

关闭时机应跟随线程的实际所有者:仅服务当前页面的线程可以在页面销毁时关闭;跨页面工作的线程应由更长生命周期的组件管理。若剩余排队任务已经失去意义,可选择 quit();若已到期任务仍需完成,则选择 quitSafely()。两者都不能撤销正在执行的业务操作。

应用主线程的 Looper 不允许退出,对它调用这两个方法会抛出 IllegalStateException。主线程消息队列的生命周期由框架管理,不能用退出主 Looper 的方式清理页面资源。

七、测试

验证时应分别观察空闲、投递和退出,而不是只看一行线程状态:

  • 在手写线程的 Looper.loop() 前后记录日志。启动后只看到前面的日志;投递消息后可以看到处理日志;正常退出后才看到后面的日志。
  • 空闲时查看线程栈,通常能看到 MessageQueue.nativePollOnce。不同工具的状态标签可能不同,不能仅凭 WAITING 或 RUNNABLE 判断它是否忙等。
  • 关闭后,在后台等待线程结束,再检查 isAlive() 或线程列表。退出方法已经返回,不代表工作线程此刻已经终止。

对比 quit() 和 quitSafely() 时,还要控制并发顺序。若先投递消息再立即从主线程退出,工作线程可能已经取走部分消息,不能断言 quit() 一定让所有消息都不执行。

更稳定的实验方式是:先让工作线程进入一个由测试控制的等待回调;确认回调已经开始后,排入立即消息和足够远期的延迟消息;接着请求退出,最后释放当前回调。在普通无屏障队列中,quit() 会丢弃仍排队的消息,quitSafely() 会继续处理已到期消息,两者都会丢弃清理时尚未到期的消息。等待控制只用于测试,必须确保最终释放。

回到最初的问题,真正需要管理的是消息线程的使用期限。

  • Looper 已经通过阻塞等待解决了空队列上的忙等问题;
  • 业务代码要补上的是停止投递、请求退出,以及在有需要时确认线程已经结束。

本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Handler 运行流程源码解析(五):quit() & quitSafely() · Yinchen 的博客