Android 屏幕刷新机制

匿名 (未验证) 提交于 2019-12-03 00:40:02
//ViewRootImpl        @Override     public void requestLayout() {         if (!mHandlingLayoutInLayoutRequest) {             checkThread();//检查是否在主线程             mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。             scheduleTraversals();         }     }       void scheduleTraversals() {         if (!mTraversalScheduled) {//同一帧内不会多次调用遍历             mTraversalScheduled = true;             mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//拦截同步Message              //探索的入口             mChoreographer.postCallback(                     Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);          }     }       final class TraversalRunnable implements Runnable {         @Override         public void run() {             doTraversal();         }     }     final TraversalRunnable mTraversalRunnable = new TraversalRunnable();        void doTraversal() {             mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);//移除同步消息屏障              performTraversals();//View的绘制流程正式开始。     }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41



针对以下 2 个要点分析

  • ① postSyncBarrier
    简单来说,同步屏障的作用是可以拦截 Looper 对同步消息的获取和分发,因为 Handler 消息机制中,Looper 会不断的从 MessageQueue 中取出 Message。加入同步屏障之后,Looper 只会获取和处理异步消息
    这么做的原因是 View 的绘制和屏幕刷新优先级肯定是最高的(也就是 VIP ,防止卡顿), 除了对View绘制渲染的处理操作可以优先处理(设置为异步消息),其它的 Message 都可以放置一边。保障弱势群体的权益。

  • ① Choreographer



Choreographer

Choreographer 是 Jelly Bean(Android 4.1)中黄油计划(Project Butter)引入的产物,包括 :

  • Choreographer : 负责统一动画、输入和绘制时机。
  • VSYNC : 垂直同步信号。
  • Triple Buffer : 第三块绘制Buffer,减少显示内容的延迟。


ViewRootImpl中,scheduleTraversals()方法调用Choreographer 的 postCallback() 方法传入将要执行遍历绘制的 runnable。

也可以这么说 : ViewRootImpl 的遍历绘制doTraversal()方法,由编舞者 Choreographer 主导,在时机成熟的时候

//Choreographer       //Posts a callback to run on the next frame.  也就是绘制下一帧的内容     public void postCallback(int callbackType, Runnable action, Object token) {         postCallbackDelayed(callbackType, action, token, 0);//延迟时间为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();             final long dueTime = now + delayMillis;              //根据时间将 action 添加到 mCallbackQueue 的队列中             mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);              if (dueTime <= now) {                 //因为传入的延迟时间delayMillis 为 0                 scheduleFrameLocked(now);             } else {                 Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);                 msg.arg1 = callbackType;                 //设置异步延迟消息 ,过dueTime后执行(无视同步屏障)                 msg.setAsynchronous(true);                 mHandler.sendMessageAtTime(msg, dueTime);             }         }     }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

postCallbackDelayedInternal()方法中,我们注意到 :

  • mCallbackQueuesdoTraversal()方法的那个runnable)。在时机成熟的时候,mCallbackQueues 会回调这些 action 。

  • setAsynchronous



紧接着上面调用的scheduleFrameLocked()方法 :

//Choreographer       private void scheduleFrameLocked(long now) {         if (!mFrameScheduled) {             mFrameScheduled = true;             if (USE_VSYNC) {//4.1及以上默认使用VSYNC垂直同步                 // If running on the Looper thread, then schedule the vsync immediately,                 // otherwise post a message to schedule the vsync from the UI thread                 // as soon as possible.                 if (isRunningOnLooperThreadLocked()) {                     scheduleVsyncLocked();// UI 线程                 } else {                     Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);                     msg.setAsynchronous(true);                     //将异步消息放置Handler队列的最前面,当前是最高优先级。                     mHandler.sendMessageAtFrontOfQueue(msg);                 }             }              ``````         }     }       private void scheduleVsyncLocked() {         mDisplayEventReceiver.scheduleVsync();     }      //只能在ui线程中使用     private final FrameDisplayEventReceiver mDisplayEventReceiver; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
//DisplayEventReceiver      /**      * Schedules a single vertical sync pulse to be delivered when the next      * display frame begins.      */     public void scheduleVsync() {         //注册一个垂直同步脉冲VSYNC,当下一个脉冲到来时会回调dispatchVsync方法         nativeScheduleVsync(mReceiverPtr);     }      private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {         onVsync(timestampNanos, builtInDisplayId, frame);     }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

从上面代码可以看出,Choreographer 即将要执行 垂直同步VSYNC信号了,如果当前在主线程,则立即调用scheduleVsyncLocked()方法,不是主线程则通过Handler(mainLooper)切换到 UI 线程,并且该Message是异步消息且放置消息队列的第一个,是最高优先级要处理的事情。


FrameDisplayEventReceiver

FrameDisplayEventReceiver 继承 DisplayEventReceiver , 主要是用来接收同步脉冲信号 VSYNC。

scheduleVsync()方法通过底层nativeScheduleVsync()向SurfaceFlinger 服务注册,即在下一次脉冲接收后会调用 DisplayEventReceiver的dispatchVsync()方法。

这里类似于订阅者模式,但是每次调用nativeScheduleVsync()方法都有且只有一次dispatchVsync()方法回调。

VSYNC



VSYNC

VSYNC 的全称是 Vertical Synchronization ,即垂直同步。

由于人眼与大脑之间的协作一般情况无法感知超过60FPS的画面更新。如果所看到画面的帧率高于12帧的时候,就会认为是连贯的,达到24帧便是流畅的体验,这也就是胶片电影的播放速度(24FPS)

对于屏幕显示,游戏体验来说,如果能整体平稳的达到60FPS,画面每秒更新60次,也就是16.67ms刷新一次,绝大部分人视觉体验都会觉得非常流畅如丝般顺滑。



Android 亦如此,正常情况下屏幕刷新频率也是60FPS

//获取手机屏幕刷新频率  Display display = getWindowManager().getDefaultDisplay(); float refreshRate = display.getRefreshRate();
  • 1
  • 2
  • 3
  • 4
//Display      public float getRefreshRate() {         return mRefreshRate;     }      //Display 内部类     public static final class Mode implements Parcelable {         private final float mRefreshRate;          public Mode(``````, float refreshRate) {             mRefreshRate = refreshRate;         }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在VirtualDisplayDevice 类中,对Mode进行赋值,并且refreshRateΪintfinal常量值,也就是60HZ

//VirtualDisplayDevice          private static final float REFRESH_RATE = 60.0f;
  • 1
  • 2
  • 3

每秒钟 60 帧的屏幕刷新频率,也就是 1000 / 60 ≈ 16.67ms 。




  1. 时间从0开始,进入第一个16ms:Display显示第0帧,CPU,GPU处理第一帧。
  2. 时间进入第二个16ms:因为早在上一个16ms时间内,第1帧已经由CPU,GPU处理完毕。Display可以正常显示第1帧。但在当前的 16ms期间,CPU和GPU却并未及时绘制第2帧数据(注意前面的空白区),而是在本周期快结束时,CPU/GPU才去处理第2帧数据。
  3. 时间进入第三个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank)。




所以关键点在于 CPU 在每个 VSYNC 信号到来时,必须开始着手处理下一帧,然后交由 GPU 处理,最后再由屏幕(display)显示出来,整个流水线作业的步调一致是保证显示系统流畅的基础。并且程序的大多数 UI 操作都要在 16.67ms内执行完成。



所以在主线程中的耗时操作会影响 ui 的流畅度。

Android Project Butter分析




dispatchVsync()方法,也就是上文所说的时机成熟的时候

//DisplayEventReceiver      /**      * Schedules a single vertical sync pulse to be delivered when the next      * display frame begins.      */     public void scheduleVsync() {         //注册一个垂直同步脉冲VSYNC,当下一个脉冲到来时会回调dispatchVsync方法         nativeScheduleVsync(mReceiverPtr);     }      private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {         onVsync(timestampNanos, builtInDisplayId, frame);     }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
//Choreographer       private final class FrameDisplayEventReceiver extends DisplayEventReceiver             implements Runnable {          @Override         public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {             ``````             mTimestampNanos = timestampNanos;//信号到来的时间参数             mFrame = frame;             Message msg = Message.obtain(mHandler, this);//this 就是当前的 run 方法             msg.setAsynchronous(true);             mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);//切换到主线程,即执行下面的 run 方法         }          @Override         public void run() {             doFrame(mTimestampNanos, mFrame);         }     }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

onVsync()run()run()

//Choreographer      void doFrame(long frameTimeNanos, int frame) {          try {             mFrameInfo.markInputHandlingStart();             doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);              mFrameInfo.markAnimationsStart();             doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);              mFrameInfo.markPerformTraversalsStart();             doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);//scheduleTraversals 方法postCallback的标识              doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);         }     }       void doCallbacks(int callbackType, long frameTimeNanos) {         ``````         try {             for (CallbackRecord c = callbacks; c != null; c = c.next) {                 c.run(frameTimeNanos);             }     }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

ViewRootImpl

//ViewRootImpl      void scheduleTraversals() {         if (!mTraversalScheduled) {//在下一帧中,只会执行一次 doTraversal 遍历操作 !             mTraversalScheduled = true;             mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();             mChoreographer.postCallback(                     Choreographer.CALLBACK_TRAVERSAL, //doCallbacks中回调CALLBACK_TRAVERSAL标识                     mTraversalRunnable, null);             ``````         }     }      final class TraversalRunnable implements Runnable {         @Override         public void run() {             doTraversal();//遍历视图 measure,layout,draw         }     }     final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

兜了一圈回到了ViewRootImpl,上面大致说明了我们当前的遍历操作,对下一帧的准备工作是,当我们ViewRootImpl
如果迟迟交不出View的绘制结果,那么屏幕将会一直显示当前画面。


Triple Buffer

Android 4.1 以前一直沿用double-buffer 双缓冲技术,也就是两块显示 Buffer,back buffer用于CPU/GPU下一帧的绘制准备,另一块 frame buffer 则用于屏幕显示,当back buffer准备就绪后,它们才进行交换。

主线程耗时阻塞,xml 布局文件层次过多冗余臃肿,绘制操作不当(onDraw中频繁创建对象)

  1. 当CPU / GPC 准备B Buffer 内容时间过长,导致第一个VSYNC信号到来时不能交付 back Buffer ,那么屏幕上显示的还是之前的那块 PRE Buffer , 并且 B Buffer 内容准备完成后,还需要等待下一个 VSYNC 信号才能交付。
  2. 因为在第二个 VSYNC 信号到来时,两块 Buffer 都已经被占用(一块用来显示 ,一块被 B Buffer 准备工作持有),所以当下一次绘制内容也存在延迟的情况也会造成连锁卡顿。(同一帧画面显示 2 次及以上)

解决上面问题的办法就是引入第三块 Buffer , 在渲染 B 超时而且 Buffer A 又用于屏幕显示时,可以用第三块 Buffer 来进行C 的准备工作,这样便减少了后面的一次 Jank 发生。

系统大部分情况下都会使用两个Buffer 来完成显示,只有在某一帧的处理时间超过 2 次 VSYNC 信号周期才会使用第三块 Buffer。



最后补上一张本文整个流程的大致时序图,下篇文章将会继续探索CPU / GPU 渲染相关知识。


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!