程序员Android开发

Choreographer机制

2021-01-16  本文已影响0人  青叶小小

一、前言

从 SDK4.1(API16)开始,Choreographer 专门负责控制同步三种事件:

这三种事件都与 UI 操作相关,代码注释中有一段说明如下:

Coordinates the timing of animations, input and drawing.
The choreographer receives timing pulses (such as vertical synchronization) from the display subsystem then schedules work to occur as part of rendering the next display frame.

翻译:

协调『动画』『输入』和『绘制』的时间。
Choreographer 收到来自显示子系统的时间脉冲(如:垂直同步信号)时,再下一帧渲染中将控制这些操作。

时间脉冲(垂直同步信号 VSYNC)是一个固定的时间,不会因任何事情而延迟到来,因此,Choreographer 基于 VSYNC 期望到达一个稳定流畅的显示效果。

理想很丰满,现实很骨感!
前端系统/框架,都将一秒显示60帧作为一个衡量流畅度的标准,即 FPS 在实际中能达到多少(最低、最高、平均),按一秒60帧来计算,那么,每帧就得控制在 1 FPS = 60 / 1000 ms = 16.67ms,如果在16.67ms中无法完成渲染,那么就需要占用额外的时间,因此就会掉帧,FPS过低,用户体验就会变差,感觉很卡。

二、Choreographer 的协调顺序

2.1、旧渲染机制

在 SDK4.1 之前,Android 的 UI 操作都是通过无间隔的向 ViewRootImpl 的 Handler 发送 Message 来处理 UI 相关的事件流程(如:输入、布局、绘制),这就会产生一个问题:FPS 取决于 ViewRootImpl 处理 Message 的执行时间,也就导致 FPS 忽高忽低(当 ViewRootImpl 的 MessageQueue 中消息太多而来不及处理 UI 相关的操作事件时,即我们常说的主线程阻塞,此时 FPS 就很低)。

2.2、新渲染机制

旧的渲染机制不稳定,亟需一种全新的、稳定的,类似 CPU 时间片的机制来定时通知 UI 相关的操作得以执行,因此也就在 SDK4.1 时引入了 Choreographer。
上节中说的三种事件,对应着 Choreographer 中的三种 Callback:

当然还有其它两种 CALLBACK(多个动画合并处理:CALLBACK_INSETS_ANIMATION = 2、本次流程的完成:CALLBACK_COMMIT = 4),但本篇主要讲的是上面三种最主要也是最重要的 CALLBACK 对应的 UI 相关操作。

Choreographer 维护着一个 CALLBACK 队列,每当下一帧到来时,Choreographer 会依次将 CALLBACK_XXXX 类型对应的 callback 取出并执行,CALLBACK_XXXX 对应的就是上面的值,即队列下标对应的 callback 回调。

理想的流程应该如下:

fluence.png

无论 Input / Animation / Draw 三种事件谁耗时长,谁耗时短,只要在一帧内处理完就行。
\color{red}{注:每次事件回调一定会调用 callback,但不一定每次都有这三种事件要处理(可能没有输入)!}

下图是掉帧的情况:

influence.png

我们可以看到,当 Draw 需要绘制太多 UI 导致下一帧到来时还没结束,则新来的 VSYNC 只好丢弃,那么我们就损失了一帧,如果经常有这样的情况发生,那么,整个界面就会让用户感觉很卡。

三、源码分析

在真式进入源码分析之前,我们还需要知道一件事:子系统(显示系统)虽然会持续发送VSYNC信号,但应用并不是无条件的接收,而是应用『主动请求』后,系统才会将最近的 VSYNC 发送给 Choreographer 并安排相应的任务;『主动请求』包括我们提到的三种事件,即:输入,动画执行,以及视图的绘制。

3.1、起源——事件的发起方

我们说了,只有主动发起,系统才会安排任务,而任务都会涉及到 UI 的操作(即最终的绘制),因此,追根溯源,事件响应后的源头还是 ViewRootImpl:

public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 注意这里:
            // callback 类型是 CALLBACK_TRAVERSAL
            // action 是 mTraversalRunnable
            mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
}

scheduleTraversals 的调用方有很多,其中,有两个方法我们在做 UI 时会经常涉及到:

这两个方法都会走到 scheduleTraversals。
关于发起方,不是本篇的重点,就聊这么多,之后会详细介绍 ViewRootImpl 。

3.2、mChoreographer.postCallback

public final class Choreographer {
    public void postCallback(int callbackType, Runnable action, Object token) {
        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,所以 dueTime = now
            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);
            }
        }
    }
}

3.3、事件回调队列:CallbackQueue

public final class Choreographer {
    // 回调队列 类比 MessageQueue,基于 time 来插入队列
    private final class CallbackQueue {
        private CallbackRecord mHead;

        // 返回一组相同到期的 callbacks => [head ~ last] 之间所有的 callback
        public CallbackRecord extractDueCallbacksLocked(long now) {
            CallbackRecord callbacks = mHead;
            ......
            CallbackRecord last = callbacks;
            CallbackRecord next = last.next;
            while (next != null) {
                if (next.dueTime > now) {
                    last.next = null;
                    break;
                }
                last = next;
                next = next.next;
            }
            mHead = next;
            return callbacks;
        }

        // 这里类似 MessageQueue 的入队,但又不同,我们来稍微回顾一下:
        // MQ 的入队是『新的 Message when 相同时会插入在所有同 when 的对象之前』
        // 而这里 callback 的入队却是在相同 dueTime 的对象之后    
        public void addCallbackLocked(long dueTime, Object action, Object token) {
            // 类似 obtainMessage ,回收池中取出空闲的,或者没有时创建一个新的 callback 对象
            CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
            
            ......
            
            while (entry.next != null) {
                // dueTime 大于等于时,继续向后遍历
                if (dueTime < entry.next.dueTime) {
                    callback.next = entry.next;
                    break;
                }
                entry = entry.next;
            }
            entry.next = callback;
        }
    
        // 移除相同 action 且 token 相同的 callback,并回收
        public void removeCallbacksLocked(Object action, Object token) {
            CallbackRecord predecessor = null;
            for (CallbackRecord callback = mHead; callback != null;) {
                final CallbackRecord next = callback.next;
                if ((action == null || callback.action == action)
                        && (token == null || callback.token == token)) {
                    if (predecessor != null) {
                        predecessor.next = next;
                    } else {
                        mHead = next;
                    }
                    recycleCallbackLocked(callback);
                } else {
                    predecessor = callback;
                }
                callback = next;
            }
        }
    }
    
    // 循环使用 callback
    private CallbackRecord obtainCallbackLocked(long dueTime, Object action, Object token) {
        CallbackRecord callback = mCallbackPool;
        if (callback == null) {
            callback = new CallbackRecord();
        } else {
            mCallbackPool = callback.next;
            callback.next = null;
        }
        callback.dueTime = dueTime;
        callback.action = action;
        callback.token = token;
        return callback;
    }
    
    // 回收 callback 
    private void recycleCallbackLocked(CallbackRecord callback) {
        callback.action = null;
        callback.token = null;
        callback.next = mCallbackPool;
        mCallbackPool = callback;
    }
}

3.4、scheduleFrameLocked

public final class Choreographer {
    private void scheduleFrameLocked(long now) {
        // 如果上一帧未结束,则当前帧丢弃
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            // 支持 vsync
            if (USE_VSYNC) {
                // 如果是当前线程的 looper 上运行,则直接 执行 vsync
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                    // 否则,抛给创建 Choreographer 线程的 looper's MQ 中
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
                // 不支持 vsync
                final long nextFrameTime = Math.max(mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
            }
        }
    }
    
    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
        // 调用 nativeScheduleVsync(mReceiverPtr);
        // 这是一个 ndk 的 native 方法
        // 用于向系统请求安排『最近』的同步信号,相当于向对应的系统进程注册了一个回调;
        // 当下一次同步信号到来时,Choreographer 的 mDisplayEventReceiver 的 onVsync 
        // 方法将被调用,该方法内部又将事件抛给了 Choreographer 的 mHandler
    }
}

3.5、Choreographer对象的创建过程

public final class Choreographer {
    // 私有静态常量对象,类加载时就初始化
    private static final ThreadLocal<Choreographer> sThreadInstance = new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            // 当前线程的 looper
            Looper looper = Looper.myLooper();
            ......
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
            
            // 如果是 UI主线程,则保存对象
            if (looper == Looper.getMainLooper()) {
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };

    private Choreographer(Looper looper, int vsyncSource) {
        mLooper = looper;
        
        // handler 绑定当前线程的 looper
        mHandler = new FrameHandler(looper);
        
        // 这里是后续接收 vsync 及处理 vsync 的地方,即3.4节中 scheduleVsyncLocked 会异步调用的
        mDisplayEventReceiver = USE_VSYNC
                ? new FrameDisplayEventReceiver(looper, vsyncSource)
                : null;
        mLastFrameTimeNanos = Long.MIN_VALUE;
    
        mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
    
        // 对应 CALLBACK_XXXX 事件的回调处理函数
        mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
        for (int i = 0; i <= CALLBACK_LAST; i++) {
            mCallbackQueues[i] = new CallbackQueue();
        }
    }
}

3.6、FrameDisplayEventReceiver.onVsync

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
    private int mFrame;

    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
        ......
        mTimestampNanos = timestampNanos;
        mFrame = frame;
         // 默认 msg.what = 0,即 MSG_DO_FRAME
        // msg.callback = this,即自己
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
        mHavePendingVsync = false;
        doFrame(mTimestampNanos, mFrame);
    }
}

3.7、(FrameHandler)mHandler

public final class Choreographer {
    private final class FrameHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DO_FRAME:
                    doFrame(System.nanoTime(), 0);
                    break;
                ......
            }
        }
    }
}

3.8、FrameDisplayEventReceiver.run

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
    private int mFrame;
    
    @Override
    public void run() {
        // 执行 doFrame
        doFrame(mTimestampNanos, mFrame);
    }
}

3.9、doFrame

这里开始,将依次执行 INPUT、ANIMATION、DRAW

public final class Choreographer {
    void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // 上一帧未完成,丢弃
            }
            ......
        }
    
        try {
            // input
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
            // animation
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
            // traversal 绘制 UI
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
            // 完成
            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } 
        ......
    }
}

3.10、doCallbacks

public final class Choreographer {
    void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            // 根据 callback 事件类型,取出该类型的一组 callbasks,详细看 3.3
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }
           ......
        }
        try {
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                c.run(frameTimeNanos); // 执行 callback 的 run
            }
        } finally {
            synchronized (mLock) {
                do {
                    final CallbackRecord next = callbacks.next;
                    // 回收 callback
                    recycleCallbackLocked(callbacks);
                    callbacks = next;
                } while (callbacks != null);
            }
        }
    }
}

这里的 c.run 就是 callback.run,而 callback 是 Runnable 类型,看到这里,大家估计已经忘记 callback 具体是什么了!

我们可以重新回过头去看一下 3.1 & 3.2 两小节,或者,我在这里再罗列下:

// callbackType 是 CALLBACK_XXXX 事件类型
// action 就是 callback( Runnable 类型)
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

// 调用方:
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    // Input 输入事件
    void scheduleConsumeBatchedInput() {
        if (!mConsumeBatchedInputScheduled && !mConsumeBatchedInputImmediatelyScheduled) {
            mConsumeBatchedInputScheduled = true;
            mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, mConsumedBatchedInputRunnable, null);
        }
    }
    
    // Animation 动画事件
    final class InvalidateOnAnimationRunnable implements Runnable {
        public void addView(View view) {
            synchronized (this) {
                mViews.add(view);
                postIfNeededLocked();
            }
        }
    
        public void addViewRect(AttachInfo.InvalidateInfo info) {
            synchronized (this) {
                mViewRects.add(info);
                postIfNeededLocked();
            }
        }
    
        private void postIfNeededLocked() {
            if (!mPosted) {
                mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
                mPosted = true;
            }
        }
    }
    
    // draw 绘制流程
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            ......
            mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
           ......
        }
    }
}

至此,我们已经完全分析完了 Choreographer 的源码,以及如何响应 VSYNC,和处理 Input、Animation 和 Draw 事件的。

四、总结

4.1、掉帧统计(系统日志)

我们经常在 UI 较重的页面,能看到 Logcat 中出现的一行类似如下警告日志:

Skipped xxxx frames! The application may be doing too much work on its main thread.

void doFrame(long frameTimeNanos, int frame) {
    ......
    synchronized (mLock) {
        ......
        long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        final long jitterNanos = startNanos - frameTimeNanos;
        if (jitterNanos >= mFrameIntervalNanos) {
            // 从上一次处理完 vsync,到本次新的 vsync,可能中间已经过去很久,
            // 通过时间差值,能够计算出『掉了多少帧』
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                // 丢失 XXXX 帧
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
            ......
        }
        ......
    }
    ......
}

注:onVsync的发生时间并非指onVsync开始执行时的时间,而是传给onVsync的参数时间,这个时间是VSYNC信号到来的时间。

关于 CALLBACK_COMMIT,这里还有个修正 VSYNC 的时间,原因如下:

如果类型是CALLBACK_COMMIT,并且当前frame渲染时间超过了两个时钟周期,则将当前提交时间修正为上一个垂直同步信号时间。为了保证下一个frame的提交时间和当前frame时间相差为一且不重复。

其根本原因在于:为了解决ValueAnimator的一个问题而引入的,主要是解决因为遍历时间过长导致动画时间启动过长,时间缩短,导致跳帧,这里修正动画第一个frame开始时间延后来改善,这时候才表示动画真正启动。为什么不直接设置当前时间而是回溯一个时钟周期之前的时间呢?看注释,这里如果设置为当前frame时间,因为动画的第一个frame其实已经绘制完成,第二个frame这时候已经开始了,设置为当前时间会导致这两个frame时间一样,导致冲突。详细情况请看官方针对这个问题的修改。Fix animation start jank due to expensive layout operations.

fixed.png

4.2、耗时监控

既然我们已经清楚整个的 Choreographer 与 ViewRootImpl (即 UI 相关的操作)的关系,那么我们也就可以做以下事情:

常用的统计方法,通常是在一个方法的开始和结束,分别插入一行打点日志,得以来统计该方法的耗时,但那是常规针对我们自己写的代码插桩而言;此处是系统方法,我们没法直接插桩,但同样也有两种方法可以来监控:

例如,我们在 Input 和 Animation 的 Callback 队列头插入我们自己的 Runnable,那么:

  • 在开始执行 Input 时,我们的第一个 Runnable 得以执行,此时记录一个时间;
  • 在开始执行 Animation时,我们的第二个 Runnable 得以执行,此时再记录个时间;
  • 两次的时间差就是执行 Input 所用到的耗时!
  • 其它依次类推!

为啥这里需要用到 COMMIT ?
因为,我们需要一个闭环,即当 TRAVERSAL 执行完后,之后就会执行 COMMIT ,这样我们就能知道 TRAVERSAL 执行了多久!具体可以参考微信 Matrix 的 UIThreadMonitor 类。

上一篇 下一篇

猜你喜欢

热点阅读