什么是同步屏障机制
同步屏障机制是一套为了让某些特殊的消息得以更快被执行的机制。
这里我们假设一个场景:我们向主线程发送了一个UI绘制操作Message,而此时消息队列中的消息非常多,那么这个Message的处理可能会得到延迟,绘制不及时造成界面卡顿。同步屏障机制的作用,是让这个绘制消息得以越过其他的消息,优先被执行。
MessageQueue中的Message,有一个变量isAsynchronous,他标志了这个Message是否是异步消息;标记为true称为异步消息,标记为false称为同步消息。同时还有另一个变量target,标志了这个Message最终由哪个Handler处理。
从Handler源码我们知道,每一个Message在被插入到MessageQueue中的时候,会强制其target属性不能为null,如下代码:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) {
// 这里要注意target指向了当前Handler
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
// 调用到了queue#enqueueMessage方法
return queue.enqueueMessage(msg, uptimeMillis);
}
这里msg.target 就会被赋值为this, 而 this 即为我们的 Handler 对象。因此,通过这种方式传进来的消息的 target 肯定也就不为 null,并且 mAsynchronous 默认为 false,也就是说我们一般发送的消息都为同步消息。
那么什么是异步消息呢,如何发送一个异步消息呢?
简单来说有两种方式。
一种是直接设置消息为异步的:
Message msg = mMyHandler.obtainMessage();
msg.setAsynchronous(true);
mMyHandler.sendMessage(msg);
还有一个需要用到 Handler 的一个构造方法,不过该方法已被标记为@Hide了:
public Handler(boolean async) {
this(null, async);
}
但在api28之后添加了两个重要的方法:
public static Handler createAsync(@NonNull Looper looper) {
if (looper == null) throw new NullPointerException("looper must not be null");
return new Handler(looper, null, true);
}
public static Handler createAsync(@NonNull Looper looper, @NonNull Callback callback) {
if (looper == null) throw new NullPointerException("looper must not be null");
if (callback == null) throw new NullPointerException("callback must not be null");
return new Handler(looper, callback, true);
}
通过这两个api就可以创建异步Handler了,而异步Handler发出来的消息则全是异步的。
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}
但是没有同步屏障,异步消息与同步消息的执行并没有什么区别。
同步屏障
同步屏障究竟有什么作用?
同步屏障为handler消息机制提供了一种优先级策略,让异步消息的优先级高于同步消息。如何开启同步屏障呢?
MessageQueue.postSyncBarrie(),该方法会往MessageQueue中插入一条同步屏障message,没有给Message赋值target属性,且插入到Message队列头部。当然源码中还涉及到延迟消息,我们暂时不关心。这个target==null的特殊Message就是同步屏障。
MessageQueue在获取下一个Message的时候,如果碰到了同步屏障,那么不会取出这个同步屏障,而是会遍历后续的Message,找到第一个异步消息取出并返回。这里跳过了所有的同步消息,直接执行异步消息。为什么叫同步屏障?因为它可以屏蔽掉同步消息,优先执行异步消息。
消息的最终处理是在消息轮询器Looper.loop()中,而loop()循环中会调用MessageQueue.next()从消息队列中取消息,来看看关键代码
Message next() {
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
Message msg = mMessages;
//如果msg.target为空,也就是说是一个同步屏障消息,则进入这个判断里面
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
//在这个while循环中,找到最近的一个异步消息
//先执行do,再执行while,所以屏障消息不会被取出
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
//如果消息的处理时间大于当前时间 则等待
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
//处理消息
mBlocked = false;
//将消息移除
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
//返回消息
return msg;
}
} else {
// No more messages.
//没有找到消息则进入阻塞状态,等待被唤醒
nextPollTimeoutMillis = -1;
}
//...
}
从上面可以看出,当执行到同步屏障消息(即标识为msg.target == null)时,消息机制优先处理异步消息。由于代码中先执行do再执行while,第一次就把指针指向了同步屏障消息的下一条消息,所以同步屏障消息会一直在消息队列中。
注意,同步屏障不会自动移除,使用完成之后需要手动进行移除,不然会造成同步消息无法被处理
同步屏障使用场景
上面我们似乎漏了一个问题:系统什么时候添加同步屏障?
异步消息需要同步屏障的辅助,但同步屏障我们无法手动添加,因此了解系统何时添加和删除同步屏障是非常必要的。只有这样,才能更好地运用异步消息这个功能,知道为什么要用和如何用。
Android 系统中更新UI就是使用同步屏障。
在 View 更新时,draw、requestLayout、invalidate 等很多地方都调用了ViewRootImpl#scheduleTraversals(),如下:
//ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//发送同步屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//发送异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
这里就发送了同步屏障消息,并发送了异步消息,由于 UI 更新相关的消息优先级是最高的,这样系统就会优先处理这些异步消息。
前面我们看到,同步屏障消息并不会自己移除,需要调用相关代码来移除同步屏障消息ViewRootImpl#unscheduleTraversals()。
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障消息
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
/**
* Removes a synchronization barrier.
*
* @param token The synchronization barrier token that was returned by
* {@link #postSyncBarrier}.
*
* @throws IllegalStateException if the barrier was not found.
*
*/
public void removeSyncBarrier(int token) {
// Remove a sync barrier token from the queue.
// If the queue is no longer stalled by a barrier then wake it.
synchronized (this) {
Message prev = null;
Message p = mMessages;
//找到同步屏障消息
while (p != null && (p.target != null || p.arg1 != token)) {
prev = p;
p = p.next;
}
if (p == null) {
throw new IllegalStateException("The specified message queue synchronization "
+ " barrier token has not been posted or has already been removed.");
}
final boolean needWake;
if (prev != null) {
prev.next = p.next; //next指向下一条消息
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
p.recycleUnchecked(); //回收同步屏障消息
// If the loop is quitting then it is already awake.
// We can assume mPtr != 0 when mQuitting is false.
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}
在绘制流程中使用同步屏障,保证了在vsync信号到来时,绘制任务可以被及时执行,避免造成界面卡顿。但这样也带来了相对应的代价:
- 我们的同步消息最多可能被延迟一帧的时间,也就是16ms,才会被执行
- 主线程Looper造成过大的压力,在VSYNC信号到来之时,才集中处理所有消息
改善这个问题办法就是:
使用异步消息。当我们发送异步消息到MessageQueue中时,在等待VSYNC期间也可以执行我们的任务,让我们设置的任务可以更快得被执行且减少主线程Looper的压力。
可能有读者会觉得,异步消息机制本身就是为了避免界面卡顿,那我们直接使用异步消息,会不会有隐患?这里我们需要思考一下,什么情况的异步消息会造成界面卡顿:异步消息任务执行过长、异步消息海量。
如果异步消息执行时间太长,那即时是同步任务,也会造成界面卡顿,这点应该都很好理解。其次,若异步消息海量到达影响界面绘制,那么即使是同步任务,也是会导致界面卡顿的;原因是MessageQueue是一个链表结构,海量的消息会导致遍历速度下降,也会影响异步消息的执行效率。所以我们应该注意的一点是:
不可在主线程执行重量级任务,无论异步还是同步。
我们以后怎么选择使用异步Handler来还是同步Handler呢?
同步Handler有一个特点是会遵循与绘制任务的顺序,设置同步屏障之后,会等待绘制任务完成,才会执行同步任务;而异步任务与绘制任务的先后顺序无法保证,在等待VSYNC的期间可能被执行,也有可能在绘制完成之后执行。因此,我的建议是:如果需要保证与绘制任务的顺序,使用同步Handler;其他,使用异步Handler。
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)