(4.6.15.4)Choreographer全解析

Posted fei20121106

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(4.6.15.4)Choreographer全解析相关的知识,希望对你有一定的参考价值。

一、前情提要

1.1 测绘流程的起点

我们在(4.1.37.1)深入理解setContentView过程和View绘制过程一文中,我们讲到了测绘流程的起点是在

  1. —ViewRoot类的requestLayout()方法
  2. —scheduleTraversals()
  3. —mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)请求刷新信号
  4. —mChoreographer内部handler通过msg机制触发mTraversalRunnable
  5. 启动performTraversals();测绘流程

那么第3步和第4步之间,是如何实现msg入队,msg响应等一系列操作的呢?

1.2 性能卡顿监听

我们在(4.6.24)构建android外网的性能监控平台Hawkeye一文中提到卡顿监听的两种策略:

  • 监听Loop中Msg的执行时长
  • 监听Choreographer两次postFrameCallback之间是否超过16ms

那么就有一些疑问了:

  1. 为什么Loop中Msg的执行时长超过1000ms就算卡顿了?我们通常说的卡顿不是说超过了16.66ms么,为何这里要超过500ms,甚至1000ms才算是卡顿?
  2. Choreographer两次postFrameCallback之间超过16ms就是卡断,但是RecyclerView的onLayout有时需要一两百毫秒那不就是会频繁报警?

二、VSYNC

1、android最高60FPS,是VSYNC决定的,每16ms最多一帧
2、VSYNC要客户端主动申请,才会有。 因此如果要通过编舞者进行性能监听,要不断的在回调里注册回调**
3、有VSYNC到来才会刷新,前提是监听者发起的Msg被执行到;
4、UI没更改,不会请求VSYNC也就不会刷新
5、UI局部重绘其实只是省去了再次构建硬件加速用的DrawOp树

2.1 单缓冲区和画面撕裂

在一个典型的显示系统中,一般包括CPU、GPU、Display三个部分, CPU负责计算帧数据,把计算好的数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数据呈现到屏幕上。如下图:

在这里插入图片描述
屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即buffer里的数据可能是来自不同的帧的, 当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。
在这里插入图片描述

简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。

2.2 双缓存区 和 Vsync

那咋解决画面撕裂呢? 答案是使用 双缓存。
由于图像绘制和屏幕读取 使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。
双缓存,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。如下图:
在这里插入图片描述
问题又来了:什么时候进行两个buffer的交换呢?
假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。
当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。那,这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现 screen tearing的状况。
VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。
所以说V-sync这个概念并不是Google首创的,它在早年的PC机领域就已经出现了

2.3 Vsync的弊端

CPU/GPU用一个Buffer, Display用一个Buffer

在这里插入图片描述
以时间的顺序来看下将会发生的过程:

  1. Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,且在Display显示下一帧前完成
  2. 因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,缓存进行交换,然后正常显示第1帧
  3. 接着第2帧开始处理,是直到第2个VSync快来前才开始处理的
  4. 第2个VSync来时,由于第2帧数据还没有准备就绪,缓存没有交换,显示的还是第1帧。这种情况被Android开发组命名为“Jank”,即发生了丢帧。
  5. 当第2帧数据准备完成后,它并不会马上被显示,而是要等待下一个VSync 进行缓存交换再显示。

所以总的来说,就是屏幕平白无故地多显示了一次第1帧。原因是 第2帧的CPU/GPU计算 没能在VSync信号到来前完成

  • 双缓存的交换 是在Vsyn到来时进行,交换后屏幕会取Frame buffer内的新数据,而实际 此时的Back buffer 就可以供GPU准备下一帧数据了。
  • 如果 Vsyn到来时 CPU/GPU就开始操作的话,是有完整的16.6ms的,这样应该会基本避免jank的出现了(除非CPU/GPU计算超过了16.6ms)。

2.4 Android的改进:收到信号就开始计算

为了优化显示性能,Google在Android 4.1系统中对Android Display系统进行了重构,实现了Project Butter(黄油工程):系统在收到VSync pulse后,将马上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU 才立刻开始计算然后把数据写入buffer。如下图:

在这里插入图片描述
一句话总结,VSync同步使得CPU/GPU充分利用了16.6ms时间,减少jank。

问题又来了,如果界面比较复杂,CPU/GPU的处理时间较长 超过了16.6ms呢?如下图:
在这里插入图片描述

  1. 在第二个时间段内,但却因 GPU 还在处理 B 帧,缓存没能交换,导致 A 帧被重复显示。
  2. 而B完成后,又因为缺乏VSync pulse信号,它只能等待下一个signal的来临。于是在这一过程中,有一大段时间是被浪费的。
  3. 当下一个VSync出现时,CPU/GPU马上执行操作(A帧),且缓存交换,相应的显示屏对应的就是B。这时看起来就是正常的。
  4. 只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复,便出现了越来越多的“Jank”。

2.5 Android的再次改进:三级缓存Buffer,更多的计算

三缓存就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。

CPU/GPU用2个Buffer, Display用一个Buffer; 譬如下图C就是第三个Buffer

在这里插入图片描述

  • 减少了Jank: 第一个Jank,是不可避免的。但是在第二个 16ms 时间段,CPU/GPU 使用 第三个 Buffer 完成C帧的计算,虽然还是会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧。

  • 带来了延迟:注意在第3段中,A帧的计算已完成,但是在第4个vsync来的时候才显示. 如果是双缓冲,那在第三个vynsc就可以显示了。

三缓冲有效利用了等待vysnc的时间,减少了jank,但是带来了延迟。 所以,是不是 Buffer 越多越好呢?这个是否定的,Buffer 正常还是两个,当出现 Jank 后三个足以。

三、编舞者Choreographer

上面讲到,Google在Android 4.1系统中对Android Display系统进行了优化:在收到VSync pulse后,将马上开始下一帧的渲染。即一旦收到VSync通知,CPU和GPU就立刻开始计算然后把数据写入buffer。

Choreographer 是 Android 4.1 google的黄油计划新增的机制,用于配合系统的 VSYNC 中断信号。其主要用途是接收系统的 VSYNC 信号,统一管理应用的输入、动画和绘制等任务的执行时机。(CALLBACK_INPUT,CALLBACK_ANIMATION,CALLBACK_TRAVERSAL,CALLBACK_COMMIT)

3.1 构造时间:ViewRootImpl实例化时(handleResumeActivity)

在这里插入图片描述

public ViewRootImpl(Context context, Display display) {
	mChoreographer = Choreographer.getInstance();
}
public static Choreographer getInstance() {
        return sThreadInstance.get();
    }

// Thread local storage for the choreographer.
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
            if (looper == Looper.getMainLooper()) {
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };

每一个Looper线程都有自己的Choreographer,其他线程发送的回调只能运行在对应Choreographer所属的Looper线程上

3.2 构造函数


    public static final int CALLBACK_INPUT = 0;//输入
    public static final int CALLBACK_ANIMATION = 1;//动画
    public static final int CALLBACK_TRAVERSAL = 2;//视图绘制
    public static final int CALLBACK_COMMIT = 3;//提交
    
 private Choreographer(Looper looper, int vsyncSource) {
        mLooper = looper;
        
        //构建指定线程的自定义handler,其实对应的是主线程
        mHandler = new FrameHandler(looper);
        // 根据是否使用了VSYNC来创建一个FrameDisplayEventReceiver对象用于请求并接收Vsync事件
        mDisplayEventReceiver = USE_VSYNC
                ? new FrameDisplayEventReceiver(looper, vsyncSource)
                : null;
                
        mLastFrameTimeNanos = Long.MIN_VALUE;//上一次帧绘制时间点
        mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());//帧绘制时间间隔,16.7ms

		// CALLBACK_LAST + 1 = 4,创建一个容量为4的CallbackQueue数组,用来存放4种不同的Callback
        mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
        for (int i = 0; i <= CALLBACK_LAST; i++) {
            mCallbackQueues[i] = new CallbackQueue();
        }
        
        // b/68769804: For low FPS experiments.
        setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
    }
  • 持有Lopper,其实对应的是主线程Lopper
  • 构建指定线程的自定义handler,其实对应的是主线程
  • 根据是否使用了VSYNC来创建一个FrameDisplayEventReceiver对象用于请求并接收Vsync事件
    • 变量USE_VSYNC用于表示系统是否是用了Vsync同步机制,该值是通过读取系统属性debug.choreographer.vsync来获取的。
  • Choreographer创建了一个大小为4的CallbackQueue队列数组,用于保存不同类型的Callback。

3.2.1 FrameHandler 内部通信器

private static final int MSG_DO_FRAME = 0; //响应引号
private static final int MSG_DO_SCHEDULE_VSYNC = 1; //请求信号
private static final int MSG_DO_SCHEDULE_CALLBACK = 2;//当需要延迟执行时

private final class FrameHandler extends Handler {
        public FrameHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DO_FRAME:
                    doFrame(System.nanoTime(), 0);
                    break;
                case MSG_DO_SCHEDULE_VSYNC:
                    doScheduleVsync();
                    break;
                case MSG_DO_SCHEDULE_CALLBACK://再次执行scheduleFrameLocked
                    doScheduleCallback(msg.arg1);
                    break;
            }
        }
    }

3.2.2 FrameDisplayEventReceiver 信号监听者

 private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

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

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
                scheduleVsync();
                return;
            }

            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;
            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.2.3 mCallbackQueues

在这里插入图片描述

3.3 核心逻辑

在这里插入图片描述

四、UI刷新流程

以Textview为例 ,当我们通过setText改变TextView内容后,UI界面不会立刻改变,APP端会先向VSYNC服务请求,等到下一次VSYNC信号触发后,APP端的UI才真的开始刷新,基本流程如下

在这里插入图片描述

在请求VSYNC的时候,会添加一个同步栅栏,防止UI线程中同步消息执行,这样做为了加快VSYNC的响应速度,如果不设置,VSYNC到来的时候,正在执行一个同步消息,那么UI更新的Task就会被延迟执行

在这里插入图片描述

4.1 请求刷新流程

在这里插入图片描述

4.1.1 View操作向上递归到ViewRootImpl.scheduleTraversals()

--View.java
 void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
            // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }

  • 同TextView类似,View内容改变一般都会调用invalidate触发视图重绘,View会递归的调用父容器的invalidateChild,逐级回溯,最终走到ViewRootImpl的invalidate
--ViewRootImpl.java
void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        scheduleTraversals();
    }
}

  • ViewRootImpl的invalidate会触发scheduleTraversals();

页面生命周期handleResumeActivity中,当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,最终回调的也是scheduleTraversals();

 // 将UI绘制的mTraversalRunnable加入到下次垂直同步信号到来的等待callback中去
 // mTraversalScheduled用来保证本次Traversals未执行前,不会要求遍历两边,浪费16ms内,不需要绘制两次
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 防止同步栅栏,同步栅栏的意思就是拦截同步消息
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // postCallback的时候,顺便请求vnsc垂直同步信号scheduleVsyncLocked
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
         <!--添加一个处理触摸事件的回调,防止中间有Touch事件过来-->
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

  • ViewRootImpl会调用scheduleTraversals准备重绘,但是,重绘一般不会立即执行,而是往Choreographer的Choreographer.CALLBACK_TRAVERSAL队列中添加了一个mTraversalRunnable,同时申请VSYNC
    • 这个mTraversalRunnable要一直等到申请的VSYNC到来后才会被执行

scheduleTraversals利用mTraversalScheduled保证:
在当前的mTraversalRunnable未被执行前,scheduleTraversals不会再被有效调用,也就是Choreographer.CALLBACK_TRAVERSAL理论上应该只有一个mTraversalRunnable的Task

4.1.2 Choreographer.postCallback请求同步信号并传入回调

public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
        
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        //缓存Callback到对应队列中CALLBACK_TRAVERSAL
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            //申请VSYNC同步信号
            scheduleFrameLocked(now);
        } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}
  • postCallbackDelayedInternal主要两个动作
    • 缓存Callback到对应队列中CALLBACK_TRAVERSAL
    • 申请VSYNC同步信号
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
        
            if (isRunningOnLooperThreadLocked()) {
               // 请求VSYNC信号,最终会调到Native层,Native处理完成后触发FrameDisplayEventReceiver的onVsync回调,回调中最后也会调用doFrame(long frameTimeNanos, int frame)方法
                scheduleVsyncLocked();
            } else {// 在UI线程上直接发送一个what=MSG_DO_SCHEDULE_VSYNC的消息,最终也会调到scheduleVsyncLocked()去请求VSYNC信号
                // 因为主线程已经有了同步栅栏,所以必须异步信息,消息才能被UI线程执行
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        }  
    }
}

  • mFrameScheduled校验:scheduleFrameLocked跟上一个scheduleTraversals类似,也采用了利用mFrameScheduled来保证:在当前申请的VSYNC到来之前,不会再去请求新的VSYNC,因为16ms内申请两个VSYNC没意义
    • mFrameScheduled保证16ms内,只会申请一次垂直同步信号
    • scheduleFrameLocked可以被调用多次,但是mFrameScheduled保证下一个vsync到来之前,不会有新的请求发出
    • 多余的scheduleFrameLocked调用被无效化
  • 触发信号请求
private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }
  • 通过mDisplayEventReceiver触发信号请求。
  • scheduleVsync()方法通过底层nativeScheduleVsync()向SurfaceFlinger 服务注册,即在下一次脉冲接收后会调用 DisplayEventReceiver的dispatchVsync()方法。
  • 这里类似于订阅者模式,但是每次调用nativeScheduleVsync()方法都有且只有一次dispatchVsync()方法回调

这里就解释了:VSYNC要客户端主动申请,才会有

4.2 接受VSYNC信号

等到VSYNC到来后,调用逻辑如下
VSYNC回来流程示意
在这里插入图片描述

  private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        public FrameDisplayEventReceiver(Looper looper) {
            super(looper);
        }

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
           
            long now = System.nanoTime();
            if (timestampNanos > now) {
            <!--正常情况,timestampNanos不应该大于now,一般是上传vsync的机制出了问题-->
                timestampNanos = now;
            }
            <!--如果上一个vsync同步信号没执行,那就不应该相应下一个(可能是其他线程通过某种方式请求的)-->
              if (mHavePendingVsync) {
                Log.w(TAG, "Already have a pending vsync event.  There should only be "
                        + "one at a time.");
            } else {
                mHavePendingVsync = true;
            }
            <!--timestampNanos其实是本次vsync产生的时间,从Native发过来-->
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            <!--由于已经存在同步栅栏,所以VSYNC到来的Message需要作为异步消息发送过去-->
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            <!--这里的mTimestampNanos其实就是本次Vynsc同步信号到来的时候,但是执行这个消息的时候,可能延迟了-->
            doFrame(mTimestampNanos, mFrame);
        }
    }

  • 在收到onVsync回调后,FrameDisplayEventReceiver将自己作为Runnable封装到一个Msg中(类似post(callabck)),用Handler发送到主线程Looper中
    • 用的是内部自定义Handler,因此处理是在主线程
    • postCallback方式,因此会回调Run函数
    • 此时主线程Loop已经开启了屏障模式,只处理异步信息,因此这里要标记是异步信息

注意此时标记了startTime
如果期间有其他异步信息被执行,会加长endTime

4.3 触发回调

doFrame执行UI绘制的示意图
在这里插入图片描述

  • 主线程Looper不断处理异步信息,知道处理到本次Msg,毁掉doFrame
void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
    <!--做了很多东西,都是为了保证一次16ms有一次垂直同步信号,有一次input 、刷新、重绘-->
        if (!mFrameScheduled) {
            return; // no work to do
        }
       long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        final long jitterNanos = startNanos - frameTimeNanos;
        <!--检查是否因为延迟执行掉帧,每大于16ms,就多掉一帧-->
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            <!--跳帧,其实就是上一次请求刷新被延迟的时间,但是这里skippedFrames为0不代表没有掉帧-->
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
            <!--skippedFrames很大一定掉帧,但是为 0,去并非没掉帧-->
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                <!--开始doFrame的真正有效时间戳-->
            frameTimeNanos = startNanos - lastFrameOffset;
        }

        if (frameTimeNanos < mLastFrameTimeNanos) {
            <!--这种情况一般是生成vsync的机制出现了问题,那就再申请一次-->
            scheduleVsyncLocked();
            return;
        }
          <!--intendedFrameTimeNanos是本来要绘制的时间戳,frameTimeNanos是真正的,可以在渲染工具中标识延迟VSYNC多少-->
        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
        <!--移除mFrameScheduled判断,说明处理开始了,-->
        mFrameScheduled = false;
        <!--更新mLastFrameTimeNanos-->
        mLastFrameTimeNanos = frameTimeNanos;
    }

    try {
         <!--真正开始处理业务-->
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
        <!--处理打包的move事件-->
        mFrameInfo.markInputHandlingStart();
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
        <!--处理动画-->
        mFrameInfo.markAnimationsStart();
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
        <!--处理重绘-->
        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
        <!--不知道干啥的-->
        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

  • doFrame也采用了一个boolean遍历mFrameScheduled保证每次VSYNC中,只执行一次
    • 可以看到,为了保证16ms只执行一次重绘,加了好多次层保障。
  • 掉帧计算
    • Vsync信号到来时记录一个开始时间,即onVsync()中的mTimestampNanos。
    • 在主线程MessageQueue中轮到doFrame()的message执行后(Vsync信号到来有可能不会立马处理,因为这中间主线程可能被其他任务占用),在doFrame()中记录一个结束时间,即下面代码中的startNanos。
    • 这两个时间差值就是Vsync信号的处理时延,即掉帧时间。
    • 超过30就打印日志
  • doFrame里除了UI重绘,其实还处理了很多其他的事,比如检测VSYNC被延迟多久执行,掉了多少帧,处理Touch事件(一般是MOVE),处理动画,以及UI
  • 当doFrame在处理Choreographer.CALLBACK_TRAVERSAL的回调时(mTraversalRunnable),才是真正的开始处理View重绘:
--ViewRootlmpl.java
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

五、性能监控中的postFrameCallback

public void postFrameCallback(FrameCallback callback) {
        postFrameCallbackDelayed(callback, 0);
    }

public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
        if (callback == null) {
            throw new IllegalArgumentException("callback must not be null");
        }

	postCallbackDelayedInternal(CALLBACK_ANIMATION,
                callback, FRAME_CALLBACK_TOKEN, delayMillis);
    }
}
  • 最终用的是CALLBACK_ANIMATION动画类型
  • 和4.1.2一样
  • <4.3 doFrame>中doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);,会触发自己定义的回调

由于doFrame的目标是16ms一次,因此我们自己的回调也会是16ms一次。

总结

UI渲染流程

  • setContentView实例化成员变量
  • handleResumeActivity中出发ViewRootlmpl实例化和setView
  • ViewRootlmpl触发requestlayout 内部触发线程验证和scheduleTraversals
  • ViewRootlmpl#scheduleTraversals向主线程中丢一个屏障信息,并调用编舞者传入回调请求Vsync信号
  • 编舞者向底层请求信号
  • 底层返回信号
  • 编舞者完善16.67ms计时,并向主线程发送异步信息
  • 异步信息被执行时,触发各类型回调(渲染、动画等)
  • ViewRootlmpl触发doTraversal(),移除屏障信息并最终通过performTraversals()开启绘制流程

丢帧(掉帧) ,是说 这一帧延迟显示 还是丢弃不再显示 ?

答:延迟显示,因为缓存交换的时机只能等下一个VSync

连续两次setTextView到底会触发几次UI重绘呢?

肯定是一次了,UI必须至少等待16ms的间隔才会绘制下一帧,所以连续两次setTextView只会触发一次重绘

如果界面没动静止了,还会刷新吗?

答:屏幕会固定, 没16.6ms刷新,CPU/GPU不走绘制流程

现在那种120HZ的,如果超过8ms会怎样呢?结果会比60HZ还差么?

超过8ms也会掉帧,但下一次缓存交换也会很快到来(8ms之内),即掉帧的时间变短了,即变细腻了

Loop中Msg的执行时长超过1000ms就算卡顿,为啥不是16.67ms?

  • 其实不是严格意义上的UI卡顿,Android的确是在16ms内刷新一帧,但是某些场景下是不需要刷新ui的,而且是16.67ms对于UI操作也太苛刻了
    • 这里其实更像是检测主线程耗时操作
    • 举个例子,我们可能在主线程操作数据库,可能在主线程解析json,可能在主线程写文件,可能在主线程做一些例如高斯模糊的耗时操作,甚至RecyclerView的layout都可能超过200ms
    • 这个时间和16ms其实就没多大关联了,我们要定义的是主线程慢操作的耗时阀值

Choreographer两次postFrameCallback之间超过16ms就是卡断,但是RecyclerView的onLayout有时需要一两百毫秒那不是会频繁报警?

  • 我们通常说的16.67ms是指两次编舞者Callback的间隔,注意是触发回调的时间(Msg已经被执行到了), 同时在当前这个msg中,ViewRootlmpl触发doTraversal()
  • 理论上两次回调应该是间隔16.67ms,除非主线程有其他任务,导致异步任务一直不能被执行。因此设置16.67ms是可行的。
  • 但是实际上一些视图元素的计算时常超过16.67ms是很正常的,因此其实是阀值层面上需要控制
05-13 16:45:39.142 1549-1549/com.lazada.android.dev E/yhf: LZD_FIRST_NET_LOAD_END1620895539142
05-13 16:45:39.171 1549-1549/com.lazada.android.dev E/yhf: LZD_FIRST_NET_PARSE_END1620895539171
05-13 16:45:39.186 1549-1549/com.lazada.android.dev E/yhf: LZD_FIRST_NET_BINDDATA_START1620895539186
05-13 16:45:39.224 1549-1549/com.lazada.android.dev E/yhf: UI线程超时(超过16ms):116ms , 丢帧:7
05-13 16:45:39.510 1549-1549/com.lazada.android.dev E/yhf: LZD_FIRST_NET_BINDDATA_END1620895539510
05-13 16:45:39.853 1549-1549/com.lazada.android.dev E/yhf: UI线程超时(超过16ms):304ms , 丢帧:19
05-13 16:45:39.933 1549-1549/com.lazada.android.dev E/yhf: UI线程超时(超过16ms):88ms , 丢帧:5

譬如上述日志,第一个vh的oncreate到最后一个vh的耗时324ms,编舞者记录了304ms丢帧19

参考文献

以上是关于(4.6.15.4)Choreographer全解析的主要内容,如果未能解决你的问题,请参考以下文章

Android图形系统(十一)-Choreographer

Vsync机制和Choreographer详解

Vsync机制和Choreographer详解

偶尔的 I/Choreographer(17165):跳过 ## 帧黑屏锁定,Android Xamarin c#

Choreographer: Skipped frames : 应用程序可能在其主线程上做太多工作

Android Studio choreographer.java