屏幕刷新机制小结

屏幕刷新机制小结

Android小彩虹2021-07-13 16:53:01130A+A-
  1. Android刷新机制
  2. SurfaceView理解

一、Android屏幕刷新机制

  1. 首先需要了解一些基本概念

    • 在一个显示系统里,一般包括CPU、GPU、Display三部分,CPU负责计算数据,把计算号的数据交给CPU,GPU会对图形数据进行渲染,渲染后放到buffer里存起来,然后Display(可称为屏幕或者显示器)负责把buffer里的数据呈现到屏幕上。显示过程,简单来说就是CPU/GPU准备好数据,存入buffer,Display每隔一段时间去buffer里面取数据,然后显示出来。Display每次读取的频率是固定的,比如16ms一次,但是CPU/GPU写数据是完全无规律的。
    • CPU计算数据指的是View树的绘制过程,也就是Activity对应视图树从根布局DecorView开始遍历View,分别执行测量、布局、绘制三个操作过程。我们常说的16.6ms刷新一次屏幕其实就是底层以固定的频率将buffer中的屏幕数据显示出来。
  2. 在之前的几篇文章里,例如 Window和WindowManager相关知识点(六) 以及 Android View相关知识点以及原理(四) 内对DecorView和setContentView以及Window的联系可以知道在onResume的时候才会创建ViewRootImpl将DecorView和Window关联起来,并且和任意View调用invalidate等刷新一样都会走ViewRootImpl中的 scheduleTraversals()方法,然后调用Choreographer的postCallBack方法

        void scheduleTraversals() {
            if (!mTraversalScheduled) {
            	//编号1
                mTraversalScheduled = true;
                //编号2
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                //编号3
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    
  3. Choreographer的postCallBack中传了一个mTraversalRunnable,大家可以看下面代码,run方法内调用了doTraversal方法

        final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }
        
       void doTraversal() {
          if (mTraversalScheduled) {
          	  //编号4
              mTraversalScheduled = false;
              //编号5
              mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
              if (mProfile) { 
                  Debug.startMethodTracing("ViewAncestor");
              }
              //这里开始对view树进行测量、布局、绘制
              performTraversals();
              if (mProfile) {
                  Debug.stopMethodTracing();
                  mProfile = false;
              }
          }
      }
    
  4. 这里会产生一个疑问,调用了scheduleTraversals方法后,代码里只是将Runnable作为参数传递到了Choreographer的postCallback方法中,要想调用doTraversal方法,那必须要有某处执行这个Runnable。为了解清楚执行逻辑,请看Choreographer的postCallback的代码

        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) {
         if (action == null) {
             throw new IllegalArgumentException("action must not be null");
         }
         if (callbackType < 0 || callbackType > CALLBACK_LAST) {
             throw new IllegalArgumentException("callbackType is invalid");
         }
         postCallbackDelayedInternal(callbackType, action, token, delayMillis);
     }
     private void postCallbackDelayedInternal(int callbackType,
                Object action, Object token, long delayMillis) {
        if (DEBUG_FRAMES) {
            Log.d(TAG, "PostCallback: type=" + callbackType
                        + ", action=" + action + ", token=" + token
                        + ", delayMillis=" + delayMillis);
        }
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            //编号6
            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方法,而传递进来的delayMillis是0,所以dueTime = now,所以调用的是scheduleFrameLocked方法,接下来看它的调用代码

     //方法
     private void scheduleFrameLocked(long now) {
            if (!mFrameScheduled) {
                mFrameScheduled = true;
                if (USE_VSYNC) {
                    if (DEBUG_FRAMES) {
                        Log.d(TAG, "Scheduling next frame on vsync.");
                    }
                    //是否在主线程
                    if (isRunningOnLooperThreadLocked()) {
                        scheduleVsyncLocked();
                    } else {
                        Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                        msg.setAsynchronous(true);
                        mHandler.sendMessageAtFrontOfQueue(msg);
                    }
                } else {
                    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);
                }
            }
        }
        //方法
        private void scheduleVsyncLocked() {
            mDisplayEventReceiver.scheduleVsync();
        }
        //方法 DisplayEventReceiver
        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 {
              nativeScheduleVsync(mReceiverPtr);
          }
      }
      //方法 FrameDisplayEventReceiver类
      private static native void nativeScheduleVsync(long receiverPtr);
    

    可以发现最终调用的是native方法,到这里就没法跟下去了,换种思路,既然之前把Runnable放到了CallbackQueue中,见第4点的编号6注释,那么调用时机一定对应于从CallbackQueue取出Runnable,也就是CallbackQueue的extractDueCallbacksLocked方法,经过查找,可以得出是doCallbacks方法内调用了该方法( CallbackQueue 是Choreographer的内部类 ) ,而调用doCallbacks方法的是doFrame方法。(这里可以看到一个callbackType对应一个Runnable队列,mCallbackQueue[callbackType])

        void doCallbacks(int callbackType, long frameTimeNanos) {
            CallbackRecord callbacks;
            synchronized (mLock) {
                // We use "now" to determine when callbacks become due because it's possible // for earlier processing phases in a frame to post callbacks that should run // in a following phase, such as an input event that causes an animation to start. final long now = System.nanoTime(); callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS); ..... } } void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { ...... try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame"); AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS); mFrameInfo.markInputHandlingStart(); doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); ...... 

    那么doFrame方法是哪里调用的呢?从之前得出了是调用FrameDisplayEventReceiver的native方法后就跟不下去了,那么进入这个类中可以发现它的 run 方法恰恰调用了doFrame方法,而官方对它其中的onVsync方法也有注释 Called when a vertical sync pulse is received ,说明这是底层回调用的。

            @Override
            public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
                if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
                    Log.d(TAG, "Received vsync from secondary display, but we don't support "
                            + "this case yet. Choreographer needs a way to explicitly request "
                            + "vsync for a specific display to ensure it doesn't lose track "
                            + "of its scheduled vsync.");
                    scheduleVsync();
                    return;
                }
                // Post the vsync event to the Handler.
                long now = System.nanoTime();
                if (timestampNanos > now) {
                    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;
                //编号7
                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);
            }
    

    从Handler的处理消息机制来看,我们知道一般如果传递了Runnable,会执行它的run方法,否则看你有没有指定Callback,有的话会执行你的Callback,没有则会调用handleMessage,所以 编号7 处,将this传递给了Message,后续将执行自己的run方法,继而执行doFrame方法,然后就会执行doCallbacks方法,然后会执行Runnable对象的run方法,run方法内又会执行doTraversals方法,这就开始刷新view咯

  5. 小结一下,FrameDisplayEventReceiver继承DisplayEventReceiver接受底层的VSync信号开始处理UI过程,VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息的内容主要就是run方法里面的doFrame了。FrameDsiplayEventReceiver之所以能收到信号,回调onVsync方法,可以理解为APP层在调用native方法nativeScheduleVsync时向底层注册了一个屏幕刷新信号监听事件,要不然底层怎么知道APP需要刷新数据呢?并且APP对底层是隐藏的,底层压根不知道APP的存在。

    梳理一下,APP通过native方法向底层注册了下一帧的屏幕刷新界面,然后在每16.6ms的帧信号到来时,它就会回调onVsync刷新屏幕了

  6. 那么问题来了,是不是在16.6ms内或者是屏幕刷新前,我可以无限注册该监听事件,也就是一帧内是不是会注册很多重复监听

    • 首先看段代码

          void scheduleTraversals() {
              if (!mTraversalScheduled) {
                  mTraversalScheduled = true;
                  //编号 8 
                  mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                  mChoreographer.postCallback(
                          Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
               ......
          }
              void doTraversal() {
              if (mTraversalScheduled) {
                  mTraversalScheduled = false;
                  //编号 9 
                  mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
      		......
          }
      
    • 首先看scheduleTraversals方法,它在向底层注册监听前,有一个mTraversalScheduled变量,该变量默认false,多次调用scheduleTraversals方法,只要mTraversalScheduled为false才会重复注册,而从doTraversal方法中可以看出只有接收到一帧信号是该变量才会重置为false。那为什么这么设计呢?查看performTraversals方法就可以知道,界面刷新调用了这个方法,方法内可以层层遍历View树的,需要刷新的View都会遍历到并刷新的,所以也就没有必要重复注册吧

  7. 接下来细心的人可能会问,在onVsync方法内的 编号7 处代码,这里会将刷新消息封装到Message里面放到主线程的MessageQueue中,而咱们的主线程也是一直在处理MessageQueue里面的消息的,同一时间只能处理一个Message,如果其它消息耗时,导致刷新消息在16.6ms内还没处理,该怎么办

    • 这种情况是避免不了的,大家只能尽量不要在这其中处理过多耗时操作
    • Android系统对其有一个优化,就是 编号 8 和 编号 9 处代码,可以先理解为设置和移除屏障消息的标识。首先是通过 编号 8 处代码会往队列里发送一个同步屏障,主Looper通过next不断取出队头的Message执行,如果队头是一个同步屏障消息时,将会遍历整个队列,寻找设置了异步标识的消息,找到该消息就取出该消息来执行,否则就让next方法陷入阻塞阶段。所以在屏障消息后面的所有同步消息都将拦截,直到通过doTraversal方法将屏障消息移除队列。这样就能保证刷新消息先执行,但是在之前取出的Message还是会依旧执行,所以说如果Message内内容耗时的话,还是会影响刷新消息的执行的。
  8. 小结

    • 界面上任意一个View的刷新请求都会调用ViewRootImpl的scheduleTraversals()方法,这个方法内会过滤一帧内的重复调用,保证同一帧内只会进行一遍View树的遍历刷新
    • 当调用scheduleTraversals()方法后,会向主线程的消息队列发送一个同步屏障,拦截这个时刻之后的所有同步消息,直到底层回调onVsync后产生的具有异步标识的刷新界面消息被执行
    • 然后会把刷新界面的操作,也就是doTraversals方法放到Runnable里,通过Choreographer的postCallback方法以时间戳的形式放到队列里面,如果当前是主线程,将直接调用native方法,如果不是主线程,将会以最高优先级,将Message放到主线程,保证尽量第一个执行该native方法
    • native的这个方法是用来向底层注册监听下一个屏幕刷新信号,当下一个屏幕刷新信号发出时,底层就会回调Choreographer的onVsync方法
    • onVsync方法调用后,由于是具有异步标识的方法,所以不会被同步屏障拦截,就能尽量保证第一时间取出并刷新界面,同时移除同步屏障
    • 最后就是执行View的遍历刷新了
    • 如果界面一直保持没变的话,也就是没有注册监听事件,但是底层的还是会以每16.6ms固定频率来切换每一帧的画面,只是最后这些画面都是相同的而已。
    • 底层是以固定的频率来切换屏幕的画面的,即使CPU计算完了数据,也就是说测量,布局,绘制等等都算完了,它也不会立即显示,而是要等到信号来的时候。

二、SurfaceView

  1. 首先列举最常见的和View的区别
    • View的绘图效率低,主要用于动画变化较少的程序,必须在主线程更新
    • SurfaceView绘图效率高,用于界面更新频繁的程序,一般在子线程更新
    • SurfaceView拥有独立的Surface(绘图表面),即它不与其宿主窗口共享同一个Surface
    • SurfaceView使用双缓冲机制,播放视频时画面更流畅
    • 每个窗口在SurfaceFlinger服务中都对应有一个Layer,用它来描述它的绘制表面。对于那些具有SurfaceView的窗口来说,每一个SurfaceView在SurfaceFlinger服务中还对应于一个独立的Layer或者LayerBuffer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面。所以SurfaceView的UI就可以在一个独立的线程中进行绘制,可以不占用主线程资源,它产生原因也是为了应对耗时的操作,例如Camera X。
  2. 部分概念
    • Canvas是Java层构建的数据结构,是给View用的画布,ViewGroup会将Canvas拆分给子View,在onDraw方法里将图形数据绘制在它获得的Canvas上
    • Surface是Native层构建的数据结构,是给SurfaceFlinger用的画布,它是直接被用来绘制到屏幕上的数据结构
    • 开发者一般所用到的View都是在Canvas进行绘制,然后最顶层的View的Canvas的数据信息会转换到一个Surface上,SurfaceFlinger会将各个应用窗口的Surface进行合成,然后绘制到屏幕上。
  3. 双缓冲机制
    • SurfaceView在更新视图时用到了两张Canvas,一张frontCanvas和一张backCanvas,每次实际显示的是frontCanvas,backCanvas存储的是上一次更改的视图。当你在播放这一帧的时候,它已经提前帮你加载好后面一帧了,所以播放起来流畅。
    • 当使用lockCanvas()获取画布时,得到的实际是backCanvas而不是正在显示的frontCanvas,之后我们再在backCanvas上绘制新的视图,再通过unlockCanvasAndPost(canvas)此视图,然后上传的这张Canvas将替换原来的frontCanvas作为新的frontCanvas,原来的frontCanvas将切换到后台作为backCanvas。相当于多线程交替解析和渲染每一帧视频数据
  4. SurfaceView是Zorder排序的,默认在宿主Window的后面,SurfaceView通过在Window上面”挖洞“(设置透明区域进行显示)
  5. SurfaceView是一个有自己的Surface的View,它的渲染可以放到单独线程而不是主线程中,其缺点是不能做变形和动画
  6. SurfaceTexture可以用作非直接输出的内容流,这样就提供二次处理的机会,与SurfaceView直接输出相比,这样会有若干帧的延迟,内存消耗也会大一些
  7. TextureView是在View hierachy中做绘制,因此它一般是在主线程做的
  8. 具体介绍可以参考 SurfaceTexture,TextureView, SurfaceView 和 GLSurfaceView 区别知多少

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们