终于搞明白了什么是同步屏障
背景
今天突然听到隔壁在讨论同步屏障,听到这个名字,我依稀记得 Handler
里面是有同步屏障机制的,但是具体的原理怎么有点模糊不清呢?就像一个明星,你明明看着面熟,就是想不起来他叫啥,让我这样的强迫症患者无比难受,所以抽时间来扒一扒同步屏障。
同步屏障机制
1. 直奔主题,同步屏障机制这几个字听起来很牛逼,能浅显的解释一下,先让大家明白它的作用是啥不?
同步屏障实际上就是字面意思,可以理解为建立一道屏障,隔离同步消息,优先处理消息队列中的异步消息进行处理,所以才叫同步屏障。
2. 第二个问题,同步消息又是啥呢?异步消息和同步消息有啥不一样呢?
要回答这个问题,我们就得了解一下 Message
,Message
的消息种类分为三种:
- 普通消息(同步消息)
- 异步消息
- 同步屏障消息
我们平时使用 Handler
发送的消息基本都是普通消息,中规中矩的排到消息队列中,轮到它了再乖乖地出来执行。
考虑一个场景,我现在往 UI 线程发送了一个消息,想要绘制一个关键的 View,但是现在 UI 线程的消息队列里面消息已经爆满了,我的这条消息迟迟都没有办法得到处理,导致这个关键 View 绘制不出来,用户使用的时候很恼怒,一气之下给出差评这是什么垃圾 app,卡的要死。
此时,同步屏障就派上用场了。如果消息队列里面存在了同步屏障消息,那么它就会优先寻找我们想要先处理的消息,把它从队列里面取出来,可以理解为加急处理。那同步屏障机制怎么知道我们想优先处理的是哪条消息呢?如果一条消息如果是异步消息,那同步屏障机制就会优先对它处理。
3.那要如何设置异步消息呢?怎样的消息才算一条异步消息呢?
Message
已经提供了现成的标记位 isAsynchronous
用来标志这条消息是不是异步消息。
4.能看看源码了解下官方到底怎么实现的吗?
看看怎么往消息队列 MessageQueue
中插入同步屏障消息吧。
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;
if (when != 0) {
// 根据when找到同步屏障消息插入的位置
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
// 插入同步屏障消息
if (prev != null) {
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
// 前面没有消息的话,同步屏障消息变成队首了
mMessages = msg;
}
return token;
}
}
在代码关键位置我都做了注释,简单来说呢,其实就像是遍历一个链表,根据 when
来找到同步屏障消息应该插入的位置。
5.同步屏障消息好像只设置了when,没有target呢?
这个问题发现了华点,熟悉 Handler
的朋友都知道,插入消息到消息队列的时候,系统会判断当前的消息有没有 target
,target
的作用就是标记了这个消息最终要由哪个 Handler
进行处理,没有 target
会抛异常。
boolean enqueueMessage(Message msg, long when) {
// target不能为空
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
...
}
问题 4 的源码分析中,同步屏障消息没有设置过 target
,所以它肯定不是通过 enqueueMessage()
添加到消息队列里面的啦。很明显就是通过 postSyncBarrier()
方法,把一个没有 target
的消息插入到消息队列里面的。
6.上面我都明白了,下面该说说同步屏障到底是怎么优先处理异步消息的吧?
OK,插入了同步屏障消息之后,消息队列也还是正常出队的,显然在队列获取下一个消息的时候,可能对同步屏障消息有什么特殊的判断逻辑。看看 MessageQueue
的 next
方法:
Message next() {
...
// msg.target == null,很明显是一个同步屏障消息
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
...
}
方法代码很长,看源码最主要还是看关键逻辑,也没必要一行一行的啃源码。这个方法中相信你一眼就发现了msg.target == null
,前面刚说过同步屏障消息的 target
就是空的,很显然这里就是对同步屏障消息的特殊处理逻辑。用了一个 do...while
循环,消息如果不是异步的,就遍历下一个消息,直到找到异步消息,也就是 msg.isAsynchronous() == true
7.原来如此,那如果消息队列中没有异步消息咋办?
如果队列中没有异步消息,就会休眠等待被唤醒。所以 postSyncBarrier()
和 removeSyncBarrier()
必须成对出现,否则会导致消息队列中的同步消息不会被执行,出现假死情况。
8.系统的 postSyncBarrier() 貌似也没提供给外部访问啊?这我们要怎么使用?
确实我们没办法直接访问 postSyncBarrier()
方法创建同步屏障消息。你可能会想到不让访问我就反射调用呗,也不是不可以。
但我们也可以另辟蹊径,虽然没办法创建同步屏障消息,但是我们可以创建异步消息啊!只要系统创建了同步屏障消息,不就能找到我们自己创建的异步消息啦。
系统提供了两个方法创建异步 Handler
:
public static Handler createAsync(@NonNull Looper looper) {
if (looper == null) throw new NullPointerException("looper must not be null");
// 这个true就是代表是异步的
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);
}
异步 Handler
发送的就是异步消息。
9.那系统什么时候会去添加同步屏障呢?
有对 View 的工作流程比较了解的朋友想必已经知道了,在 ViewRootImpl
的 requestLayout
方法中,系统就会添加一个同步屏障。
不了解也没关系,这里我简单说一下。
(1)创建 DecorView
当我们启动了 Activity 后,系统最终会执行到 ActivityThread
的 handleLaunchActivity
方法中:
final Activity a = performLaunchActivity(r, customIntent);
这里我们只截取了重要的一行代码,在 performLaunchActivity
中执行的就是 Activity 的创建逻辑,因此也会进行 DecorView
的创建,此时的 DecorView
只是进行了初始化,添加了布局文件,对用户来说,依然是不可见的。
(2)加载 DecorView 到 Window
onCreate
结束后,我们来看下 onResume
对应的 handleResumeActivity
方法:
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
...
// 1.performResumeActivity 回调用 Activity 的 onResume
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
...
final Activity a = r.activity;
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
// 2.获取 decorview
View decor = r.window.getDecorView();
// 3.decor 现在还不可见
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 4.decor 添加到 WindowManger中
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
}
...
}
注释 4 处,DecorView
会通过 WindowManager
执行了 addView()
方法后加载到 Window
中,而该方法实际上是会最终调用到 WindowManagerGlobal
的 addView()
中。
(3)创建 ViewRootImpl 对象,调用 setView() 方法
// WindowManagerGlobal.ddView()
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
WindowManagerGlobal
的 addView()
会先创建一个 ViewRootImpl
实例,然后将 DecorView
作为参数传给 ViewRootImpl
,通过 setView()
方法进行 View 的处理。setView()
的内部主要就是通过 requestLayout
方法来请求开始测量、布局和绘制流程。
(4)requestLayout() 和 scheduleTraversals()
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 主要方法
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
// 1.将mTraversalScheduled标记为true,表示View的测量、布局和绘制过程已经被请求。
mTraversalScheduled = true;
// 2.往主线程发送一个同步屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 3.注册回调,当监听到VSYNC信号到达时,执行该异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
看到了吧,注释 2 的代码熟悉的很,系统调用了 postSyncBarrier()
来创建同步屏障了。那注释 3 是啥意思呢?mChoreographer
是一个 Choreographer
对象。
要理解 Choreographer
的话,还要明白 VSYNC
。
我们的手机屏幕刷新频率是 1s 内屏幕刷新的次数,比如 60Hz、120Hz 等。60Hz表示屏幕在一秒内刷新 60 次,也就是每隔 16.6ms 刷新一次。屏幕会在每次刷新的时候发出一个 VSYNC
信号,通知CPU进行绘制计算,每收到 VSYNC
,CPU 就开始处理各帧数据。这时 Choreographer
就上场啦,当有 VSYNC
信号到来时,会唤醒 Choreographer
,触发指定的工作。它提供了一个回调功能,让业务知道 VSYNC
信号来了,可以进行下一帧的绘制了,也就是注释 3 使用的 postCallback
方法。
当监听到 VSYNC
信号后,会回调来执行 mTraversalRunnable
这个 Runnable
对象。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// View的绘制入口方法
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
在这个 Runnable
里面,会移除同步屏障。然后调用 performTraversals
这个View 的工作流程的入口方法完成对 View 的绘制。
这回明白了吧,系统会在调用 requestLayout()
的时候创建同步屏障,等到下一个 VSYNC
信号到来时才会执行相应的绘制任务并移除同步屏障。所以在等待 VSYNC
信号到来的期间,就可以执行我们自己的异步消息了。
参考
“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解
来源:juejin.cn/post/7258850748150104120