我们都清楚自行启动 Looper 的线程,在任务结束时需要手动调用 quit() 或 quitSafely() 终止 Looper 轮循。但对于其中细节似乎没有仔细思考过,抽上五分钟简要学习下!
Looper 线程为什么要手动 quit?
quit 时 Message 们怎么处置?
quitSafely 做了哪些优化?
主线程 Looper 需要 quit 吗?
Looper 线程为什么要手动 quit?
创建 Looper 并执行 loop() 的线程在任务结束的时候,需要手动调用 quit。
反之,线程将由于 loop() 的轮询一直处于可运行状态,CPU 资源无法释放。更有可能因为 Thread 作为 GC Root 持有超出生命周期的实例引发内存泄漏。
当 quit 调用后,Looper 不再因为没有 Message 去等待,而是直接取到为 null 的 Message,这将触发轮循死循环的退出。
// Looper.java public static void loop() { ... for (;;) { Message msg = queue.next(); if (msg == null) { // 拿到 null 则退出 return; } ... } }
quit 时 Message 们怎么处置?
Looper 的很多处理实则都是 MessageQueue
在发挥作用,包括这里的 Looper#quit()。它其实是调用 MessageQueue 的同名函数 quit(boolean),并指定 safe
参数为 false
。
// Looper.java public void quit() { // 默认是不安全的退出 mQueue.quit(false); }
MessageQueue#quit()
则主要执行几项简单工作,包括:标记正在退出,并清空所有未执行的 Mesage,最后唤醒线程。
// MessageQueue.java void quit(boolean safe) { ... synchronized (this) { ... mQuitting = true; // 标记 quitting // 不安全的退出将回收队列中所有 Message,并清空队列 if (safe) { removeAllFutureMessagesLocked(); } else { removeAllMessagesLocked(); } // 唤醒线程 nativeWake(mPtr); } }
- 退出的标记将导致后续的
sendMessage()
或postRunnable()
失效,直接返回false
。
// MessageQueue.java boolean enqueueMessage(Message msg, long when) { ... synchronized (this) { ... if (mQuitting) { IllegalStateException e = new IllegalStateException( msg.target + " sending message to a Handler on a dead thread"); Log.w(TAG, e.getMessage(), e); msg.recycle(); // quitting flag 导致后续的 Message send 失败 return false; } ... } return true; }
- 默认的策略是清空队列里所有 Message,包括时间正好抵达的 Message 都无法处理,不太
友好安全。
// MessageQueue.java // 一刀切:无论 when 是否抵达都出队 // 可能当前时刻本该执行的 Message 也会被剔除,无法执行 private void removeAllMessagesLocked() { Message p = mMessages; while (p != null) { Message n = p.next; p.recycleUnchecked(); p = n; } mMessages = null; }
- 最后唤醒线程,进入读取队列的下一次循环,因为队列已无 Message,将直接返回 null。
// MessageQueue.java Message next() { ... for (;;) { ... synchronized (this) { ... // 队列已无 Message // 将因为行退出标志的存在直接返回 null if (mQuitting) { dispose(); return null; } ... } ... } }
- loop() 拿到的 Message 为 null,死循环退出,线程结束。
quitSafely 做了哪些优化?
大家都知道 SDK 更推荐使用 quitSafely() 去终止 Looper,原因在于其只会剔除执行时刻 when 晚于当前调用时刻的 Message。
这样可以保证 quitSafely 调用的那刻,满足执行时间条件的 Message 继续保留在队列中,在都执行完毕才退出轮询。
调用 MessageQueue#quit(),并指定 safe 参数为 true。
// Looper.java public void quitSafely() { mQueue.quit(true); }
- 安全 quit 的时候调用
removeAllFutureMessagesLocked()
。
// MessageQueue.java void quit(boolean safe) { synchronized (this) { if (safe) { // 安全退出的调用 removeAllFutureMessagesLocked(); } else { removeAllMessagesLocked(); } nativeWake(mPtr); } }
顾名思义, 其只会移除未来 Message。※未来
是 when 相较于当前时刻而言,不是没执行的都叫作未来,不要误解!
// MessageQueue.java private void removeAllFutureMessagesLocked() { final long now = SystemClock.uptimeMillis(); Message p = mMessages; if (p != null) { // 如果队首 Message 的执行时刻仍晚于当前时刻,那么全部清空 if (p.when > now) { removeAllMessagesLocked(); } else { // 否则遍历队列,筛选需要剔除的 Message Message n; for (;;) { n = p.next; // 没有更晚的 Message,均不需要剔除,直接返回 if (n == null) { return; } // 找到队列中最前一个晚于当前时刻的 Message if (n.when > now) { break; } p = n; } // 前一个 Message 后全部出队 p.next = null; // 将最前一个晚于当前时刻的 Message 及之后的 Message 回收 do { p = n; n = p.next; p.recycleUnchecked(); } while (n != null); } } }
- 移除未来 Message 之后唤醒线程的 next() 循环,其将取出留在队列里的 Message 进行处理。
Message next() { ... for (;;) { ... synchronized (this) { final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; ... if (msg != null) { // 队列里的 Message 早于当前时间,进入else if (now < msg.when) { nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { mBlocked = false; // Message 出队并更新指向 if (prevMsg != null) { prevMsg.next = msg.next; } else { // 队首后移一个节点 mMessages = msg.next; } // 拿到了 Message,并交给 Looper 回调 msg.next = null; msg.markInUse(); return msg; } } else { ... } // 队列里仍残存 Message // 暂时不会因为退出标志返回 null if (mQuitting) { dispose(); return null; } } ... } }
- 等残存 Message 都执行完了,下一次轮询的 next() 将取不到 Message,最终因为
quitting
flag 返回 null,进而触发 loop() 死循环的退出。
主线程 Looper 需要 quit 吗?
主线程 ActivityThread 创建 Looper 时指定了不允许 quit 的标志,即不可以手动调用 quit。
// Looper.java public static void prepareMainLooper() { prepare(false); ... } private static void prepare(boolean quitAllowed) { ... sThreadLocal.set(new Looper(quitAllowed)); } // Main Looper 初始化的时候指定了不允许退出 private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); ... }
如果强行在主线程里调用了 quit(),会发生如下异常:
java.lang.IllegalStateException: Main thread not allowed to quit.
// MessageQueue.java void quit(boolean safe) { if (!mQuitAllowed) { throw new IllegalStateException("Main thread not allowed to quit."); } ... }
那主线程需要 quit 吗?
不需要,在内存不足的时候 App 由 AMS 直接回收进程。
不需要 quit 的原因在于?
主线程极为重要,承载着 ContentProvider、Activity、Service 等组件生命周期的管理,即便某个组件结束了,它仍有继续存在去调度其他组件的必要!
换言之,ActivityThread 的作用域超过了这些组件,不该由这些组件去处理它的结束。 比如,Activity destroy 了,ActivityThread 仍然要处理其他 Activity 或 Service 等组件的事务,不能结束。
结语
基于回收资源或避免内存泄漏的考虑,在 Thread 完成任务后应当手动 quit。而为了确保 quit 时本可以执行的 Message 能安全执行,尽量调用 quitSafely。同时搞清楚主线程 Looper 的重要性,不需要也不可以被手动 quit!