用力一瞥Android渲染机制-黄油计划

Posted 丶笑看退场

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用力一瞥Android渲染机制-黄油计划相关的知识,希望对你有一定的参考价值。

一. 渲染基本概念

对于渲染来说在开始前我们先了解几个概念:

CPU主要负责包括 MeasureLayoutRecordExecute 的计算操作。

GPU主要负责 Rasterization(栅格化)操作。栅格化是指将向量图形格式表示的图像转换成位图(像素)以用于显示设备输出的过程,简单来说就是将我们要显示的视图,转换成用像素来表示的格式。

帧率代表了GPU在一秒内绘制操作的帧数。

刷新率代表了屏幕在一秒内刷新屏幕的次数,android手机一般为60HZ。

二. Android黄油计划

涉及到滑动流畅,Android在谷歌4.1版本引入了黄油计划。其中有三个重要的核心元素:VSYNC、缓存区和Choreographer:

2.1 VSYNC信号

在Android4.0的时候,CPU可能会因为在忙其他的事情,导致没来得及处理UI绘制。为了解决这个问题,设计成系统在收到VSYN信号后,才会开始下一帧的渲染。也就是收到VSYN通知,CPU和GPU才开始计算然后把数据写入buffer中。

VSYN信号是由屏幕产生的,并且以60fps的固定频率发送给Android系统,在Android系统中的SurfaceFlinger接收发送的Vsync信号。当屏幕从缓存区扫描完一帧到屏幕上之后,开始扫描下一帧之前,发出的一个同步信号,该信号用来切换前缓冲区和后缓冲区。

在引入了Vsyn信号之后,绘制就变成了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DcWnksFU-1664701709409)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2d7f6c7ca88547f6b424d120635da7a0~tplv-k3u1fbpfcp-watermark.image?)]

可以看到渲染的时候从第0帧开始,CPU开始准备第一帧的图形处理,好了才交给GPU进行处理,再上一帧到来之后,CPU就会开始第二帧的处理,基本上跟Vsync的信号保持同步。

有了Vsync机制,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。

2.2 三重缓存

在采用双缓冲机制的时候,也意味着有两个缓存区,分别是让绘制和显示器拥有各自的buffer,GPU使用Back Buffer进行一帧图像数据写入,显示器则是用Frame Buffer,一般来说CPU和GPU处理数据的速度视乎都能在16ms内完成,而且还有时间空余。但是一旦界面比较复杂的情况,CPU/GPU的处理时间超过了16ms,双缓冲开始失效了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MmBD8MFY-1664701709412)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3b3a8ab39c0c4d11a56cac997047e507~tplv-k3u1fbpfcp-watermark.image?)]

在第二个时间段内,因为GPU还是处理B帧,数据没有及时交换,导致继续系那是之前A缓存区中的内容。

在B帧完成之后,又因为缺少了Vusnc信号,只能等待一段时间。

直到下一个Vsync信号出现的时候,CPU/GPU才开始马上执行,由于执行时间仍然超过了16ms,导致下一次应该执行的缓存区交换又被推迟了,反复这种情形,就会出越来越多的jank。

为了解决这个问题,Android 4.1才引入了三缓冲机制:在双缓冲机制的基础上增加了一个Graohic Buffer缓冲区,这样就可以最大限度的利用空闲的时间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MHvDEdXr-1664701709413)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1a1f6fe8d05c4fa5ba8272232562491f~tplv-k3u1fbpfcp-watermark.image?)]

可以看到在第二个时间段里有了区别,在第一次Vsync发生之后,CPU不用再等待了,它会使用第三个bufferC来进行下一帧的准备工作。整个过程就开始的时候卡顿了一下,后面还是很流畅的。但是GPU需要跨越两个Vsync信号才能显示,这样就还是会有一个延迟的现象。

总的来说三缓冲有效利用了等待vysnc的时间,减少了jank,但是带来了lag。

2.3 Choreographer

在了解了Vsync机制后,上层又是如何接受这个Vsync信号的?

Google为上层设计了一个Choreographer类,翻译成中文是“编舞者”,是希望通过它来控制上层的绘制(舞蹈)节奏。

可以直接从其构造函数开始看起:

private Choreographer(Looper looper, int vsyncSource) 
  //创建Looper对象
        mLooper = looper;
  //接受处理消息
        mHandler = new FrameHandler(looper);
  //用来接受垂直同步脉冲,也就是Vsync信号
        mDisplayEventReceiver = USE_VSYNC
                ? new FrameDisplayEventReceiver(looper, vsyncSource)
                : null;
        mLastFrameTimeNanos = Long.MIN_VALUE;
//计算下一帧的时间,Androoid手机屏幕是60Hz的刷新频率
        mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
//初始化CallbackQueue,将在下一帧开始渲染时回调
        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));
    

主要来看下FrameHandlerFrameDisplayEventReceiver的数据结构:

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;
                //请求Vsync信号
                case MSG_DO_SCHEDULE_VSYNC:
                    doScheduleVsync();
                    break;
                //请求执行Callback
                case MSG_DO_SCHEDULE_CALLBACK:
                    doScheduleCallback(msg.arg1);
                    break;
            
        
    

FrameHandler可以看到对三种消息进行了处理,对其具体实现一会分析。

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, CONFIG_CHANGED_EVENT_SUPPRESS);
        
  
        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) 
            
            ......
            mTimestampNanos = timestampNanos;
            mFrame = frame;
          //将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息
            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);
        
    

可以看出来这个类主要是用来接收底层的VSync信号开始处理UI过程。而Vsync信号是由SurfaceFlinger实现并定时发送,接收到之后就会调用onVsync方法,在里面进行处理消息发送到主线程处理,另外在run()方法里面执行了doFrame(),这也是接下来要关注的重点方法。

2.3.1 Choreographer执行过程

ViewRootImpl 中调用 Choreographer 的 postCallback 方法请求 Vsync 并传递一个任务(事件类型是 Choreographer.CALLBACK_TRAVERSAL)

最开始执行的是postCallBack发起回调,这个FrameCallback将会在下一帧渲染时执行。而其内部又调用了postCallbackDelayed方法,在其中又调用了postCallbackDelayedInternal方法:

private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) 
        ......
        synchronized (mLock) 
            final long now = SystemClock.uptimeMillis();
            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);
            
        
    

在这里执行了时间的计算,如果立即就会调用scheduleFrameLocked方法,不然就会延迟发送一个MSG_DO_SCHEDULE_CALLBACK消息,并且在这里使用msg.setAsynchronous(true)讲消息设置成异步。、

而所对应的mHandle也就是之前的FrameHandler,根据消息类型MSG_DO_SCHEDULE_CALLBACK,最终会调用到doScheduleCallback方法:

void doScheduleCallback(int callbackType) 
        synchronized (mLock) 
            if (!mFrameScheduled) 
                final long now = SystemClock.uptimeMillis();
                if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) 
                    scheduleFrameLocked(now);
                
            
        
    

到了这一步看到还是会调用到scheduleFrameLocked方法。

private void scheduleFrameLocked(long now) 
        if (!mFrameScheduled) 
            mFrameScheduled = true;
            if (USE_VSYNC) 
              //开启了Vsync
                if (DEBUG_FRAMES) 
                    Log.d(TAG, "Scheduling next frame on vsync.");
                

                
                if (isRunningOnLooperThreadLocked()) 
                  //申请Vsync信号
                    scheduleVsyncLocked();
                 else 
                  //最终还是会调用到scheduleVsyncLocked方法
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                
             else 
              //如果没有直接使用Vsync的话,则直接通过该消息执行doFrame
                final long nextFrameTime = Math.max(
                        mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
                if (DEBUG_FRAMES) 
                    Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
                
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
            
        
    

在这里对是否使用Vsync信号进行处理,如果没有使用则直接通过消息执行doFrame。如果使用的就会先判断是否在当前Looper线程中运行,如果在的话就会请求Vsync信号,否则发送消息到 FrameHandler。直接来看下scheduleVsyncLocked方法:

 private void scheduleVsyncLocked() 
        mDisplayEventReceiver.scheduleVsync();
    

可以看到调用了FrameDisplayEventReceiverscheduleVsync方法,通过查找在其父类DisplayEventReceiver中找到了scheduleVsync方法:

public void scheduleVsync() 
        if (mReceiverPtr == 0) 
            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                    + "receiver has already been disposed.");
         else 
          //申请VSYNC信号,会回调onVsunc方法
            nativeScheduleVsync(mReceiverPtr);
        
    

scheduleVsync()就是使用native方法nativeScheduleVsync()去申请VSYNC信号。等下一次信号接收后会调用dispatchVsync 方法:

private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame) 
        onVsync(timestampNanos, physicalDisplayId, frame);
    

这个onVsync方法最终实现也就是在FrameDisplayEventReceiver里。可以知道最终还是走到了doFrame方法里。

void doFrame(long frameTimeNanos, int frame) 
        final long startNanos;
        synchronized (mLock) 
            if (!mFrameScheduled) 
                return; // no work to do
            
           ......
					//设置当前frame的Vsync信号到来时间     
            long intendedFrameTimeNanos = frameTimeNanos;
            startNanos = System.nanoTime();
            final long jitterNanos = startNanos - frameTimeNanos;
            if (jitterNanos >= mFrameIntervalNanos) 
              //时间差大于一个时钟周期,认为跳frame    
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
              //跳frame数大于默认值,打印警告信息,默认值为30   
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) 
                    Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                            + "The application may be doing too much work on its main thread.");
                
              //计算实际开始当前frame与时钟信号的偏差值  
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                if (DEBUG_JANK) 
                    Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                            + "which is more than the frame interval of "
                            + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                            + "Skipping " + skippedFrames + " frames and setting frame "
                            + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
                
              
              //修正偏差值,忽略偏差,为了后续更好地同步工作 
                frameTimeNanos = startNanos - lastFrameOffset;
            

          //若时间回溯,则不进行任何工作,等待下一个时钟信号的到来
            if (frameTimeNanos < mLastFrameTimeNanos) 
                if (DEBUG_JANK) 
                    Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "
                            + "previously skipped frame.  Waiting for next vsync.");
                
              //请求下一次时钟信号  
                scheduleVsyncLocked();
                return;
            

            ......

           //记录当前frame信息    
            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
            mFrameScheduled = false;
           //记录上一次frame开始时间,修正后的  
            mLastFrameTimeNanos = frameTimeNanos;
        

        try 
          //执行相关callBack   
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
         finally 
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        
    

doFrame方法对当前帧的运行时间进行了一系列判断和修正,最终顺序执行了五种事件回调。

  1. CALLBACK_INPUT:输入
  2. CALLBACK_ANIMATION:动画
  3. CALLBACK_INSETS_ANIMATION:插入更新的动画
  4. CALLBACK_TRAVERSAL:遍历,执行measure、layout、draw
  5. CALLBACK_COMMIT:遍历完成的提交操作,用来修正动画启动时间

接着就会执行doCallbacks方法:

void doCallbacks(int callbackType, long frameTimeNanos) 
    CallbackRecord callbacks;
    ......
    try 
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
      //迭代执行所有队列任务
        for (CallbackRecord c = callbacks; c != null; c = c.next) 
            .....
          //调用CallbackRecord内的run方法
            c.run(frameTimeNanos);
        
     finally 
        synchronized (mLock) 
            mCallbacksRunning = false;
            do 
                final CallbackRecord next = callbacks.next;
                recycleCallbackLocked(callbacks);
                callbacks = next;
             while (callbacks != null);
        
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    

主要是去遍历CallbackRecrd,执行所有任务:

private static final class CallbackRecord 
        public CallbackRecord next;
        public long dueTime;
        public Object action; // Runnable or FrameCallback
        public Object token;

        @UnsupportedAppUsage
        public void run(long frameTimeNanos) 
            if (token == FRAME_CALLBACK_TOKEN) 
                ((FrameCallback)action).doFrame(frameTimeNanos);
             else 
                ((Runnable)action).run();
            
        
    

最终actionrun方法会被执行,这里的action也就是我们在前面调用psetCallback传进来的,也就是 ViewRootImpl 发起的绘制任务mTraversalRunnable了。

然后这里又一次调用了doFrame方法,在啥时候token会是FRAME_CALLBACK_TOKEN呢? 可以发现在我们调用postFrameCallback内部会调用postCallbackDelayedInternal进行赋值:

 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);
    

ChoreographerpostFrameCallback()通常用来计算丢帧情况。

知道了Choreographer是上层用来接收VSync的角色之后,我们需要进一步了解VSync信号是如何控制上层的绘制的。而绘制UI的起点是View的requestLayout或者是invalidate方法被调用触发,好了时间不早了,这些就放在下一篇Android的屏幕刷新机制里解释吧。(刷新流程和同步屏障)

三. 小结

Android在黄油计划中引入了三个核心元素:VSYNCTriple BufferChoreographer

VSYNC 信号是由屏幕(显示设备)产生的,并且以 60fps 的固定频率发送给 Android 系统,Android 系统中的 SurfaceFlinger 接收发送的 VSYNC 信号。VSYNC 信号表明可对屏幕进行刷新而不会产生撕裂。

三重缓存机制(Triple Buffer) 利用 CPU/GPU 的空闲等待时间提前准备好数据,有效的提升了渲染性能。

又介绍了 Choreographer ,它实现了协调动画(animations)、输入(input)、绘制(drawing)三个UI相关的操作。

参考

Android 显示刷新机制、VSYNC和三重缓存机制

Android图形显示系统(一)

Android屏幕刷新机制

Android Choreographer 源码分析

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

以上是关于用力一瞥Android渲染机制-黄油计划的主要内容,如果未能解决你的问题,请参考以下文章

用力一瞥Android渲染机制-黄油计划

Android黄油计划之Choreographer原理解析

面试专题Android屏幕刷新机制

面试专题Android屏幕刷新机制

Chromium网页渲染机制简要介绍和学习计划

Chromium网页渲染机制简要介绍和学习计划