前言
在之前的文章中《面试官:如何提高 Message 的优先级》关于如何提高 Message 的优先级提出个人的理解,有大佬在后面评论所提到的同步屏障+异步消息
也能提高 Message 的优先级。首先表示感谢,而外也发觉自己对同步屏障的理解还不够,所以,赶紧研究研究。
同步屏障与异步消息说明
首先,我们得了解下,什么是同步屏障,它跟异步消息又有什么关系?这里的同步和异步又是什么?
这里,我们又要涉及到了 Handler 机制。Android 真是博大精深,而且变化贼快,越学越怀疑自己当年是不是入错行了。
不过大家也不用担心,我不会一下就上源码,毕竟觉得还是理论逻辑比较重要,源码只是实践证明。
ok,正文开始:
- 在 MessageQueue 中,通过链表形式对 Message 进行存储,并通过 when 的大小对 Message 进行排序。
- Looper 循环遍历 MessageQueue 获取 Message,并且拿 Message 的 target 变量出来,调用 target 的 dispatchMessage 方法。而这个 target 变量其实就是发送 Message 的 Handler。
这一切原本都是很正常的执行着,但是总是会有一些特别的需求。
例如某些 Message 需要立即执行。但是大家都知道 MessageQueue 是链表的形式存储并等待 Looper 遍历执行的,并不像 Thread 争夺非公平锁一样,一上来就有机会抢夺。所以,在链表中要做到优先执行,就有两种途径:
- 将 Message 消息插入到 MessageQueue 的前面,这样就能让 Looper 早点拿到,早点执行了。详情同样可以看看这篇文章《面试官:如何提高 Message 的优先级》
- 还有另外一种途径,那就是 Looper 在获取 Message 的时候,暂停直接读取第一个 Message,而是对于 MessageQueue 进行遍历,找出里面优先级最高的进行执行,执行完这些贵宾后,再到普通群众。
同步屏障与异步消息的使用其实就是第二种途径。那 Looper 什么时候知道要启动贵宾模式呢?
源码的做法是 Looper 获取得到 Message 的时候,发现其 target 变量为 null,这就是触发条件。这其实就是同步屏障,产生屏障,阻止后续的同步消息执行。
而 Looper 如何知道哪些是贵宾?这又涉及到另外一个判断条件,那就是 Message 里面有个isAsynchronous()
方法,假如返回值为 true,则说明是贵宾,也就是异步消息,false 则说明是普通群众,也就是同步消息。
这里的同步和异步跟我们之前所理解的同步和异步不太一样,我们之前所理解的异步一般为并发执行,例如在主线程中开启子线程A,主线程和子线程能够不断争夺 CPU 调度,会产生交错执行的效果,但是这里的同步和异步仅仅为一个标识。
源码验证
通过上述的说明,预计同学们已经对同步屏障与异步消息有了个基础的认识了,假如还不能理解的话,建议再次看下。
好了,下面开始源码分析,你准备好了吗?
同步消息
同步消息其实就是发送默认的 Message:
- sendMessage
- sendEmptyMessage
- post
- sendMessageDelayed
- sendEmptyMessageDelayed
- postDelayed
- sendMessageAtFrontOfQueue
- postAtFrontOfQueue
他们最终都会调用到enqueueMessage
,然后给 Message 的 target 进行赋值:
Handler.java
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
//给 target 赋值
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
//判断有没有开启异步
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
复制代码
而且由于 Handler 没有开启异步,所以mAsynchronous
默认为 false,也就是默认为同步消息。
mAsynchronous
设置的地方有三个:
- public Handler(boolean async)
- public Handler(@Nullable Callback callback, boolean async)
- public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async)
这三个都是 Handler 的构造方法,通过async
直接赋值给mAsynchronous
,不过的话,这三个构造方法都是 hide 的,我们无法直接访问。
异步消息
异步消息其实跟同步消息差不多,只不过的话,是先构建 Message,然后调用setAsynchronous(true)
var message = Message.obtain()
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
message.setAsynchronous(true)
}
复制代码
该方法需要 SDK 的版本大于等于 22 才能使用,也就是 Android 5.1。
发送同步屏障
同步屏障其实就是一个 Message 消息,只不过 target 为 null 而已。当然,不能使用上面所描述的消息发送方法,因为它们会自动给 target 赋值了,发送同步屏障有自己的方法:
postSyncBarrier()
,在MessageQueue.java
中,不能被应用直接调用。
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
synchronized (this) {
//这个 token 我们要稍微记录下,后续解除屏障会用到
final int token = mNextBarrierToken++;
//创建屏障消息,注意在这里并没有给 target 进行赋值,所以 target = null
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
//将屏障消息插入到 MessageQueue 队列中
//同样是通过 when 进行排序
Message prev = null;
Message p = mMessages;
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;
}
}
复制代码
不过,看完上面源码,我还是有个疑问,为什么屏障消息的 when 为SystemClock.uptimeMillis()
,而不是为 0,既然如此的话,那还是会优先处理这两个方法所发送的消息,再处理屏障消息的:
- sendMessageAtFrontOfQueue
- postAtFrontOfQueue
若有哪位大佬知道,麻烦告知,谢谢。
获取屏障消息和异步消息
获取屏障消息和异步消息的代码逻辑其实并不是在 Looper 的 loop() 中的,在 loop() 中仅仅是调用Message msg = queue.next();
获取 Message,具体是什么消息,Looper 才不管,拿到 Message 后,只管使用msg.target
,连判空都没有。
public static void loop() {
···
final MessageQueue queue = me.mQueue;
···
for (;;) {
//获取 Message 消息
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
···
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
···
try {
msg.target.dispatchMessage(msg);
···
}
···
}
}
复制代码
所以,获取屏障消息和异步消息,我们都要看MessageQueue.java
里面的Message next()
:
Message next() {
···
for (;;) {
···
synchronized (this) {
//获取 Message 消息
Message prevMsg = null;
Message msg = mMessages;
//发现为屏障消息
if (msg != null && msg.target == null) {
// 进入循环当中,直到成功获取异步消息
// 所以这里要特别注意,假如只发送屏障,后续没有清除屏障就会进入死循环
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
···
return msg;
···
}
}
复制代码
清除屏障消息
从源码分析中,我们了解到,假如只发送屏障消息不清除,后续就会进入死循环,所以,我们在处理完异步消息后,需要把屏障消息清除掉:
MessageQueue.java
的removeSyncBarrier(int token)
,不能被应用直接调用:
public void removeSyncBarrier(int token) {
synchronized (this) {
Message prev = null;
Message p = mMessages;
// 遍历整个链表,寻找屏障消息
while (p != null && (p.target != null || p.arg1 != token)) {
prev = p;
p = p.next;
}
···
// 当已经触发了屏障消息,那么屏障消息就是在第一位,所以 prev 为null
if (prev != null) {
···
} else {
//重新给当前消息赋值,其实就是移除屏障消息
mMessages = p.next;
···
}
···
}
}
复制代码
这里有用到我们发布同步屏障时的 token,所以在发布同步屏障的时候,需要记录下来。
源码终于分析完了,相信大家都有更深一步了解了,那么,我们实战检验下。
实战同步屏障与异步消息
大致思路如下:
- 发送一个延迟 1 秒的同步消息
- 发送一个延迟 3 秒的异步消息
- 开启同步屏障
- 查看日志输出
- 当然,执行完异步消息后,还需要关闭同步屏障
由于代码难度并不大,我就全贴出来,另外,由于部分方法不能直接访问,我就通过反射的方式进行调用:
val myHandler = Handler(Looper.getMainLooper())
//存储发布同步屏障的 token
var token = 0;
//同步消息
myHandler.postDelayed({
Log.e("TAG", "我是同步消息,延迟 1 秒发送")
}, 1000)
//异步消息
var syncMessage = Message.obtain(myHandler){
Log.e("TAG", "我是异步消息,延迟 3 秒发送")
//移除同步屏障
val method = MessageQueue::class.java.getDeclaredMethod("removeSyncBarrier", Int::class.java)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
method.invoke(Looper.getMainLooper().queue, token)
}
}
//设置为异步消息
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
syncMessage.setAsynchronous(true)
}
myHandler.sendMessageDelayed(syncMessage, 3000)
//发送同步屏障
val method = MessageQueue::class.java.getDeclaredMethod("postSyncBarrier")
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
token = method.invoke(Looper.getMainLooper().queue) as Int
}
复制代码
运行:
E/TAG: 我是异步消息,延迟 3 秒发送
E/TAG: 我是同步消息,延迟 1 秒发送
复制代码
和预料中的一样,即使异步消息发送慢,并且延迟时间更久,但是还是优先执行异步消息。
放弃
以上就是同步屏障与异步消息的内容了,不过的话,不太建议在线上使用,毕竟风险太大。
- 方法的使用依靠反射,后续官方可能会限制。
- 由于不正确的使用,导致同步屏障没有被消除,导致 ANR。可以看看《今日头条 ANR 优化实践系列 – Barrier 导致主线程假死》。
- 使用
sendMessageAtFrontOfQueue
或postAtFrontOfQueue
代替。
所以,大家重点去理解去逻辑以及实现即可。
什么?那学来干啥?
这,你问问面试官。或许会有更好的回答。
猜你喜欢
- 面试官:如何提高Message的优先级
- 制作一个永远不会崩溃的App
- 自定义Gradle Plugin+字节码插桩
- 从手写ButterKnife到掌握注解、AnnotationProcessor
- 来,带你手写个性能检测工具
创作不易,你的点赞是我最大的支持。