注册

终于搞明白了什么是同步屏障

背景


今天突然听到隔壁在讨论同步屏障,听到这个名字,我依稀记得 Handler 里面是有同步屏障机制的,但是具体的原理怎么有点模糊不清呢?就像一个明星,你明明看着面熟,就是想不起来他叫啥,让我这样的强迫症患者无比难受,所以抽时间来扒一扒同步屏障。


同步屏障机制


1. 直奔主题,同步屏障机制这几个字听起来很牛逼,能浅显的解释一下,先让大家明白它的作用是啥不?


同步屏障实际上就是字面意思,可以理解为建立一道屏障,隔离同步消息,优先处理消息队列中的异步消息进行处理,所以才叫同步屏障。


2. 第二个问题,同步消息又是啥呢?异步消息和同步消息有啥不一样呢?


要回答这个问题,我们就得了解一下 MessageMessage 的消息种类分为三种:



  • 普通消息(同步消息)
  • 异步消息
  • 同步屏障消息

我们平时使用 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 的朋友都知道,插入消息到消息队列的时候,系统会判断当前的消息有没有 targettarget 的作用就是标记了这个消息最终要由哪个 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,插入了同步屏障消息之后,消息队列也还是正常出队的,显然在队列获取下一个消息的时候,可能对同步屏障消息有什么特殊的判断逻辑。看看 MessageQueuenext 方法:


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 的工作流程比较了解的朋友想必已经知道了,在 ViewRootImplrequestLayout 方法中,系统就会添加一个同步屏障。


不了解也没关系,这里我简单说一下。


(1)创建 DecorView


当我们启动了 Activity 后,系统最终会执行到 ActivityThreadhandleLaunchActivity 方法中:


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 中,而该方法实际上是会最终调用到 WindowManagerGlobaladdView() 中。


(3)创建 ViewRootImpl 对象,调用 setView() 方法


// WindowManagerGlobal.ddView()
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);

WindowManagerGlobaladdView() 会先创建一个 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 信号到来的期间,就可以执行我们自己的异步消息了。


参考


requestLayout竟然涉及到这么多知识点


关于Handler同步屏障你可能不知道的问题


“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解


作者:搬砖的代码民工
来源:juejin.cn/post/7258850748150104120

0 个评论

要回复文章请先登录注册