Android 全面解析之 Handler 机制:常见问题汇总,三级缓存和二级缓存的区别
// 初始化主线程 Looper
Looper.prepareMainLooper();
...
// 新建一个 ActivityThread 对象
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
// 获取 ActivityThread 的 Handler,也是他的内部类 H
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
...
Looper.loop();
// 如果 loop 方法结束则抛出异常,程序结束
throw new RuntimeException("Main thread loop unexpectedly exited");
}
main 方法中先初始化主线程 Looper,新建 ActivityThread 对象,然后再启动 Looper,这样主线程的 Looper 在程序启动的时候就跑起来了。我们不需要再去初始化主线程 Looper。
[](
)为什么主线程的 Looper 是一个死循环,但是却不会 ANR?
答: 因为当 Looper 处理完所有消息的时候会进入阻塞状态,当有新的 Message 进来的时候会打破阻塞继续执行。
这其实没理解好 ANR 这个概念。ANR,全名 Application Not Responding。当我发送一个绘制 UI 的消息到主线程 Handler 之后,经过一定的时间没有被执行,则抛出 ANR 异常。Looper 的死循环,是循环执行各种事务,包括 UI 绘制事务。Looper 死循环说明线程没有死亡,如果 Looper 停止循环,线程则结束退出了。Looper 的死循环本身就是保证 UI 绘制任务可以被执行的原因之一。同时 UI 绘制任务有同步屏障,可以更加快速地保证绘制更快执行。同步屏障下面会讲。
[](
)Handler 如何保证 MessageQueue 并发访问安全?
答:循环加锁,配合阻塞唤醒机制。
我们可以发现 MessageQueue 其实是“生产者-消费者”模型,Handler 不断地放入消息,Looper 不断地取出,这就涉及到死锁问题。如果 Looper 拿到锁,但是队列中没有消息,就会一直等待,而 Handler 需要把消息放进去,锁却被 Looper 拿着无法入队,这就造成了死锁。Handler 机制的解决方法是循环加锁。在 MessageQueue 的 next 方法中:
Message next() {
.
..
for (;;) {
...
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
...
}
}
}
我们可以看到他的等待是在锁外的,当队列中没有消息的时候,他会先释放锁,再进行等待,直到被唤醒。这样就不会造成死锁问题了。
那在入队的时候会不会因为队列已经满了然后一边在等待消息处理一边拿着锁呢?这一点不同的是 MessageQueue 的消息没有上限,或者说他的上限就是 JVM 给程序分配的内存,如果超出内存会抛出异常,但一般情况下是不会的。
[](
)Handler 是如何切换线程的?
答: 使用不同线程的 Looper 处理消息。
前面我们聊到,代码的执行线程,并不是代码本身决定,而是执行这段代码的逻辑是在哪个线程,或者说是哪个线程的逻辑调用的。每个 Looper 都运行在对应的线程,所以不同的 Looper 调用的 dispatchMessage 方法就运行在其所在的线程了。
[](
)Handler 的阻塞唤醒机制是怎么回事?
答: Handler 的阻塞唤醒机制是基于 Linux 的阻塞唤醒机制。
这个机制也是类似于 handler 机制的模式。在本地创建一个文件描述符,然后需要等待的一方则监听这个文件描述符,唤醒的一方只需要修改这个文件,那么等待的一方就会收到文件从而打破唤醒。
[](
)能不能让一个 Message 加急被处理?/ 什么是 Handler 同步屏障?
答:可以 / 一种使得异步消息可以被更快处理的机制
如果向主线程发送了一个 UI 更新的操作 Message,而此时消息队列中的消息非常多,那么这个 Message 的处理就会变得缓慢,造成界面卡顿。所以通过同步屏障,可以使得 UI 绘制的 Message 更快被执行。
什么是同步屏障?这个“屏障”其实是一个 Message,插入在 MessageQueue 的链表头,且其 target==null。Message 入队的时候不是判断了 target 不能为 null 吗?不不不,添加同步屏障是另一个方法:
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
// 把当前需要执行的 Message 全部执行
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
// 插入同步屏障
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
可以看到同步屏障就是一个特殊的 target,哪里特殊呢?target==null,我们可以看到他并没有给 target 属性赋值。那这个 target 有什么用呢?看 next 方法:
Message next() {
...
// 阻塞时间
int nextPollTimeoutMillis = 0;
for (;;) {
...
// 阻塞对应时间
nativePollOnce(ptr, nextPollTimeoutMillis);
// 对 MessageQueue 进行加锁,保证线程安全
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
/**
1
*/
if (msg != null && msg.target == null) {
// 同步屏障,找到下一个异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// 下一个消息还没开始,等待两者的时间差
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 获得消息且现在要执行,标记 MessageQueue 为非阻塞
mBlocked = false;
/**
2
*/
// 一般只有异步消息才会从中间拿走消息,同步消息都是从链表头获取
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
// 没有消息,进入阻塞状态
nextPollTimeoutMillis = -1;
}
// 当调用 Looper.quitSafely()时候执行完所有的消息后就会退出
if (mQuitting) {
dispose();
return null;
}
...
}
...
}
}
这个方法我在前面讲过,我们重点看一下关于同步屏障的部分,看注释 1 的地方的代码:
if (msg != null && msg.target == null) {
// 同步屏障,找到下一个异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
如果遇到同步屏障,那么会循环遍历整个链表找到标记为异步消息的 Message,即 isAsynchronous 返回 true,其他的消息会直接忽视,那么这样异步消息,就会提前被执行了。注释 2 的代码注意一下就可以了。
注意,同步屏障不会自动移除,使用完成之后需要手动进行移除,不然会造成同步消息无法被处理。从源码中可以看到如果不移除同步屏障,那么他会一直在那里,这样同步消息就永远无法被执行了。
有了同步屏障,那么唤醒的判断条件就必须再加一个:MessageQueue 中有同步屏障且处于阻塞中,此时插入在所有异步消息前插入新的异步消息。这个也很好理解,跟同步消息是一样的。如果把所有的同步消息先忽视,就是插入新的链表头且队列处于阻塞状态,这个时候就需要被唤醒了。看一下源码:
boolean enqueueMessage(Message msg, long when) {
...
// 对 MessageQueue 进行加锁
synchronized (this) {
...
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
/**
1
*/
// 当线程被阻塞,且目前有同步屏障,且入队的消息是异步消息
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
/**
2
*/
// 如果找到一个异步消息,说明前面有延迟的异步消息需要被处理,不需要被唤醒
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p;
prev.next = msg;
}
// 如果需要则唤醒队列
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
同样,这个方法我之前讲过,把无关同步屏障的代码忽视,看到注释 1 处的代码。如果插入的消息是异步消息,且有同步屏障,同时 MessageQueue 正处于阻塞状态,那么就需要唤醒。而如果这个异步消息的插入位置不是所有异步消息之前,那么不需要唤醒,如注释 2。
那我们如何发送一个异步类型的消息呢?有两种办法:
使用异步类型的 Handler 发送的全部 Message 都是异步的
给 Message 标志异步
Handler 有一系列带 Boolean 类型的参数的构造器,这个参数就是决定是否是异步 Handler:
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
// 这里赋值
mAsynchronous = async;
}
在发送消息的时候就会给 Message 赋值:
评论