深入浅出Handler(四)同步屏障

236 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

一、前言

之前的Android消息机制中,我们知道了MessageQueue中的Message是按照message.when顺序以链表的形式排列的,主线程一直在处理MessageQueue里的Message。

主线程在同一时间只能处理一个Message.

那对于Android这样一个前台与用户展示与交互比较繁重的系统,不管是系统开机还是启动一个app。

这里面都有大量的绘制任务.此时主线程肯定已经收到各种繁重任务的message,我们希望绘制任务的message提前执行或者有一些优先级别较高的message,此时该如何告诉Looper呢

在深入探讨这个问题之前我们需要先提前了解一下Android的刷新机制

二、显示系统

一个通用的显示系统一般分为CPU,GPU,Display三个部分

  • CPU负责计算数据,把计算好的数据交给GPU
  • GPU会对图形数据进行渲染,渲染好后放到buffer里
  • Display负责把buffer里的数据呈现到屏幕上

三、Android的Choreographer的实现原理

3.1 invalidate()

Activity中的布局首次绘制,以及每次调用view的invalidate()时

public void invalidate() {
    invalidate(true);
}


public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
   ...

   ....
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            p.invalidateChild(this, damage);
        }

        // Damage the entire projection receiver, if necessary.
        if (mBackground != null && mBackground.isProjected()) {
            final View receiver = getProjectionReceiver();
            if (receiver != null) {
                receiver.damageInParent();
            }
        }
    }
}

3.2 invalidateChild()

子view的invalidate(),最终走到了父布局的invalidateChild()方法

@Deprecated
@Override
public final void invalidateChild(View child, final Rect dirty) {
    final AttachInfo attachInfo = mAttachInfo;
    ...
    ViewParent parent = this;
    if (attachInfo != null) {
        ....

        do {
            View view = null;
            if (parent instanceof View) {
                view = (View) parent;
            }

            ...

            parent = parent.invalidateChildInParent(location, dirty);
            ...
        } while (parent != null);
    }
}

3.3 invalidateChildInParent()

通过do while循环查找父布局,最终指向了顶层父布局ViewRootImpl的invalidateChildInParent方法

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    checkThread();
    ...

    invalidateRectOnScreen(dirty);

    return null;
}
private void invalidateRectOnScreen(Rect dirty) {
    ...
    
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        scheduleTraversals();
    }
}

3.4 scheduleTraversals()

View的invalidate()方法中,通过一系列的条件判断和循环查找父布局的工作,最终走到了ViewRootImpl的scheduleTraversals()方法里.

其实,invalidate(),requestLayout(),requestFocus(),以及设置背景,设置动画等与view相关的刷新操作,最终都会调用view的scheduleTraversals()方法


void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

这里面用到了我们熟悉的handler.getlooper

mHandler.getLooper().getQueue().postSyncBarrier();

同时还传入了一个Runnable

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

3.5 doTraversal()

void doTraversal() {
    if (mTraversalScheduled) {
        ...

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

3.6 performTraversals()

private void performTraversals() {
    
    ...

          {
                   //View的Measure
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                }

                layoutRequested = true;
            }
        }
    } 

    final boolean didLayout = layoutRequested && (!mStopped || wasReportNextDraw);
  
   ...
    if (didLayout) {
       //View的layout方法
        performLayout(lp, mWidth, mHeight); 
    ...
       //view的draw方法
        performDraw();
    } else {
      
            ....
}

可以看到view的测量measure,布局layout,绘制draw三大流程都是在performTraversals方法中进行的.

四、Choreographer

mChoreographer.postCallback(
        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

此时我们来看一下mChoreographer这个对象

4.1 postCallback()


public void postCallback(int callbackType, Runnable action, Object token) {
    //此时delayMillis为0
    postCallbackDelayed(callbackType, action, token, 0);
}

public void postCallbackDelayed(int callbackType,
        Runnable action, Object token, long delayMillis) {
    ...

    postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    ...

    synchronized (mLock) {
        //当前时间
        final long now = SystemClock.uptimeMillis();
        //delayMillis=0
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            //两者相等
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

可以看到postCallbackDelayedInternal()方法会根据当前时间戳将这个Runnable(action)保存到mCallbackQueues队列里.

4.2 scheduleFrameLocked()


@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769497)
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
        "debug.choreographer.vsync", true);

private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
        //这个变量一直是true
            ...
            if (isRunningOnLooperThreadLocked()) {
            //是否是当前线程
                scheduleVsyncLocked();
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } ....
        }
    }
}

4.4 scheduleVsyncLocked()


private void scheduleVsyncLocked() {
   ...
        mDisplayEventReceiver.scheduleVsync();
  ....
}

4.5 scheduleVsync()


DisplayEventReceiver.java

private static native void nativeScheduleVsync(long receiverPtr);

public void scheduleVsync() {
   ...
        nativeScheduleVsync(mReceiverPtr);
   ...
}

此时已经走到了native层的nativeScheduleVsync方法

小结

到这里,我们知道当一个View发起刷新操作时,会层层通知ViewRootImpl的 scheduleTraversals方法,然后这个方法会将view的测量,布局,绘制三大方法封装成performTraversals()封装到Runnable里.

传给Choreographer,并且将当前的时间戳放进了mCallbackQueues队列里.并且调用了一个native方法.

所以暂时我们还不知道这个runnable何时不执行

五、mCallbackQueues

既然这个方法被封装成Runnable并且放进了mCallbackQueues,那它肯定会被执行,我们先看下这个队列的取操作

private final class CallbackQueue {
    private CallbackRecord mHead;

    public boolean hasDueCallbacksLocked(long now) {...
    }

    //队列取出
    public CallbackRecord extractDueCallbacksLocked(long now) {
       ...
    }

   //入队列操作
    public void addCallbackLocked(long dueTime, Object action, Object token) {
    ...
    }

    //移除操作
    public void removeCallbacksLocked(Object action, Object token) {
        ...
    }
}

5.1 extractDueCallbacksLocked()

看下这个extractDueCallbacksLocked()方法是何时被取出的.

void doCallbacks(int callbackType, long frameTimeNanos, long frameIntervalNanos) {
    CallbackRecord callbacks;
    synchronized (mLock) {
        ...
        final long now = System.nanoTime();
        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                now / TimeUtils.NANOS_PER_MS);
        if (callbacks == null) {
            return;
        }
        mCallbacksRunning = true;

       ...
}

5.2 doCallbacks()

再来看下doCallbacks是哪里调用的

void doFrame(long frameTimeNanos, int frame,
        DisplayEventReceiver.VsyncEventData vsyncEventData) {
    ...

            ...
            //对应于之前postCallBack的入参
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos, frameIntervalNanos);

  ....
}

...
mChoreographer.postCallback(
        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

5.3 doFrame()

当doFrame()被调用并且对应的条件是Choreographer.CALLBACK_TRAVERSAL时,就可以执行我们的view绘制方法了,我们来看下到底有几处方法调用了doFrame()

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;
    private VsyncEventData mLastVsyncEventData = new VsyncEventData();

    public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
        super(looper, vsyncSource, 0);
    }

    // TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
    // the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
    // for the internal display implicitly.
    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
            VsyncEventData vsyncEventData) {
        try {
            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW,
                        "Choreographer#onVsync " + vsyncEventData.id);
            }
            // Post the vsync event to the Handler.
            // The idea is to prevent incoming vsync events from completely starving
            // the message queue.  If there are no messages in the queue with timestamps
            // earlier than the frame time, then the vsync event will be processed immediately.
            // Otherwise, messages that predate the vsync event will be handled first.
            long now = System.nanoTime();
            if (timestampNanos > now) {
                Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                        + " ms in the future!  Check that graphics HAL is generating vsync "
                        + "timestamps using the correct timebase.");
                timestampNanos = now;
            }

            if (mHavePendingVsync) {
                Log.w(TAG, "Already have a pending vsync event.  There should only be "
                        + "one at a time.");
            } else {
                mHavePendingVsync = true;
            }

            mTimestampNanos = timestampNanos;
            mFrame = frame;
            mLastVsyncEventData = vsyncEventData;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

    @Override
    public void run() {
        mHavePendingVsync = false;
        //在run方法里会从mCallbackQueue中取出消息并按照时间戳顺序调用mTraversalRunnable的run函数.
        doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
    }
}

这一篇文章我们先不涉及Native的Vsnnc信号是如何回调给Java层.到这里我们知道View的绘制方法是封装在Runnable接收到信号时回调执行的.

六、同步消息屏障

我们知道Android是基于消息机制的,每一个操作都是Message,如果在触发绘制的时候,此时消息队列中还有很多消息还没执行,此时如何保证Vsync信号和绘制的同步,提高这个绘制消息的优先级呢?

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //核心代码
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

再回过头看一下当初发送绘制消息时,ViewRootImpl是如何调用的.

很明显,我们在使用handler时好像没接触过postSyncBarrier();

6.1 postSyncBarrier()

private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    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) {
            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;
    }
}

可以看到,这个方法只是创建了一个Message,并且像之前的msg enqueueMessage加入消息链表一样,但是这个msg没有设置target.

//普通的handler发送消息 会将msg.target与当前发送消息的handler进行一个绑定
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

这样一个没有和handler进行绑定的Msg到底有什么用呢?

6.2 MessageQueue.next()

Message next() {
    ...
    for (;;) {
        ...
        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            
            //此时遇到msg.target为null的消息
            if (msg != null && msg.target == null) {
                //do while 循环遍历消息链表
                //msg指向离表头最近的一个异步消息,此时跳出循环
                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();
                    //返回msg
                    return msg;
                }
            } else {
               ...
            }

           ....

        ...
    }
}

可以看到,当我们设置了同步屏障之后,next函数将会忽略所有的msg.isAsynchronous()同步消息,返回异步消息.

在这个优先级机制下,异步消息的优先级是高于同步消息的.

而我们也可以在之前的postCallBack中看到设置了给绘制任务设置了异步消息的设置


public void postCallback(int callbackType, Runnable action, Object token) {
    //此时delayMillis为0
    postCallbackDelayed(callbackType, action, token, 0);
}

public void postCallbackDelayed(int callbackType,
        Runnable action, Object token, long delayMillis) {
    ...

    postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    ...

    synchronized (mLock) {
        //当前时间
        final long now = SystemClock.uptimeMillis();
        //delayMillis=0
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            //两者相等
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            //设置异步消息
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

可以看到,在Android源码中,很多与View绘制相关的类中都调用了这种设置异步消息的方法. image.png

6.3 removeSyncBarrier

我们知道postSyncBarrier设置同步屏障只是为了告诉Looper此时消息队列里有异步消息处理,需要尽快找到异步消息并返回执行.

那当我们找到异步消息并开始执行之后,此时就要拆掉此次的同步屏障,恢复之前的消息执行次序.

可以看到在doTraversal方法中

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
       
 //此时拆除同步屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        ...执行绘制任务
        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

七、总结

我们都知道MessageQueue中的消息是以链表的形式按照msg.when的大小依次排列的.而对于有些优先级较高比如一些view绘制刷新动作,android内部采用了同步异步消息以及消息屏障的机制,提高了消息的优先级/

当view需要执行绘制任务,会把这个异步消息封装成异步的message时,先给Looper发送一个同步屏障,当next()方法在取到一个同步屏障的时候,就会遍历整个队列,寻找添加了异步消息标志的消息.一直到找到这个异步消息,此时开始执行这个优先级最高的异步消息,并且拆除同步屏障.

7.1 彩蛋

看到这里,可能有朋友要问了,既然这个消息优先级这么高,为啥不立马把消息放到队列里执行啊?链表是按照时间排序的啊,早点进去就能早点执行,干嘛还费这么大劲给一个后插入的消息,还得提高优先级,早进去不就行了?

7.2 屏幕刷新机制

开头我们只是简单的介绍了一下android的view刷新机制,试想如果同一时刻各种不同view一直在刷新,那系统该如何控制view的绘制频率呢?

所以Android底层会定时的发送屏幕刷新信号,也就是我们熟悉的16.6ms,这是因为手机屏幕的刷新频率是60hz,一秒内刷新60次,也就是每隔16.6ms刷新一次,此时就会回调我们之前走到的view绘制方法调用.

所以当我们告诉系统需要刷新的时候,系统也不是立马展开绘制任务,而是给主线程设置一个同步屏障,告诉Looper此时有优先级较高的消息处理,并且监听屏幕刷新信号,当Vsync信号监听到时,会发送一个异步消息给主线程Handler,此时执行我们需要刷新的绘制任务,并且移除同步屏障.

而正因为这种定时刷新机制也提高了绘制效率

7.3 丢帧

既然定时定频率的刷新,怎么还会丢帧呢?

造成丢帧一般有两种原因

  • View的绘制方法超过了16.6ms,下一帧的屏幕刷新信号已经来了,可是此时绘制方法还没有执行结束

  • 主线程一直在处理其他耗时的操作,导致无法读到同步屏障,无法读到这个异步消息.

第一个原因就是我们的布局嵌套或者绘制时间过长,需要我们去优化布局

第二个原因就是我们说的避免在主线程中执行耗时操作.

所以在发送同步信号,以及接受到屏幕刷新信号之前主线程是不是啥都没干,一直在等异步任务进来?--- 答案是的.因为对于App来说,这种绘制的任务优先级最高,所以在收到同步屏障消息之后,为了尽可能保证及时进行View的绘制工作,主线程的确在等待此时优先级很高的绘制任务到来.

至于Android底层的屏幕刷新信号到底是如何回调的,以及更多FrameWork层源码解析,后续都会慢慢更新~