Looper 需要手动 quit,那主线程 Looper 呢?
我们都清楚自行启动 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!