布景

今日突然听到近邻在评论同步屏障,听到这个姓名,我依稀记得 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 全面理解