在上一篇 View的工作流程 的博客中,分析了ViewRootImpl类中应用窗口 measure,layout 和 draw 的过程。今天这篇文章探索从ViewRootImpl 到屏幕的刷新之间的渊源。
针对以下 2 个要点分析
① postSyncBarrier : Handler 的同步屏障 简单来说,同步屏障的作用是可以拦截 Looper 对同步消息的获取和分发,因为 Handler 消息机制中,Looper 会不断的从 MessageQueue 中取出 Message。加入同步屏障之后,Looper 只会获取和处理异步消息,如果没有异步消息那么就会进入阻塞状态。 这么做的原因是 View 的绘制和屏幕刷新优先级肯定是最高的(也就是 VIP ,防止卡顿), 除了对View绘制渲染的处理操作可以优先处理(设置为异步消息),其它的 Message 都可以放置一边。保障弱势群体的权益。
① Choreographer : 编舞者。(系统工程师认为 View的渲染绘制就是指尖上的艺术,每一次交互都是一场视觉盛宴,创作过程中增添几分意境)
Choreographer 是 Jelly Bean(Android 4.1)中黄油计划(Project Butter)引入的产物,包括 :
Choreographer : 负责统一动画、输入和绘制时机。VSYNC : 垂直同步信号。Triple Buffer : 第三块绘制Buffer,减少显示内容的延迟。在ViewRootImpl中,scheduleTraversals()方法调用Choreographer 的 postCallback() 方法传入将要执行遍历绘制的 runnable。
也可以这么说 : ViewRootImpl 的遍历绘制doTraversal()方法,由编舞者 Choreographer 主导,在时机成熟的时候,Choreographer 会回调callback方法,View开始遍历绘制 measure –> layout –> draw 。
//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); } } }在postCallbackDelayedInternal()方法中,我们注意到 :
mCallbackQueues : callback回调queue会根据时间添加入action(也就是触发doTraversal()方法的那个runnable)。在时机成熟的时候,mCallbackQueues 会回调这些 action 。
setAsynchronous : 异步消息。因为主线程的消息机制中已经添加了同步屏障,所以Handler只会处理异步消息。
紧接着上面调用的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; //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); }从上面代码可以看出,Choreographer 即将要执行 垂直同步VSYNC信号了,如果当前在主线程,则立即调用scheduleVsyncLocked()方法,不是主线程则通过Handler(mainLooper)切换到 UI 线程,并且该Message是异步消息且放置消息队列的第一个,是最高优先级要处理的事情。
FrameDisplayEventReceiver 继承 DisplayEventReceiver , 主要是用来接收同步脉冲信号 VSYNC。
scheduleVsync()方法通过底层nativeScheduleVsync()向SurfaceFlinger 服务注册,即在下一次脉冲接收后会调用 DisplayEventReceiver的dispatchVsync()方法。
这里类似于订阅者模式,但是每次调用nativeScheduleVsync()方法都有且只有一次dispatchVsync()方法回调。
那么,这个同步脉冲信号 VSYNC 到底有何作用呢。
VSYNC 的全称是 Vertical Synchronization ,即垂直同步。
由于人眼与大脑之间的协作一般情况无法感知超过60FPS的画面更新。如果所看到画面的帧率高于12帧的时候,就会认为是连贯的,达到24帧便是流畅的体验,这也就是胶片电影的播放速度(24FPS)
对于屏幕显示,游戏体验来说,如果能整体平稳的达到60FPS,画面每秒更新60次,也就是16.67ms刷新一次,绝大部分人视觉体验都会觉得非常流畅如丝般顺滑。
Android 亦如此,正常情况下屏幕刷新频率也是60FPS
//获取手机屏幕刷新频率 Display display = getWindowManager().getDefaultDisplay(); float refreshRate = display.getRefreshRate(); //Display public float getRefreshRate() { return mRefreshRate; } //Display 内部类 public static final class Mode implements Parcelable { private final float mRefreshRate; public Mode(``````, float refreshRate) { mRefreshRate = refreshRate; }在VirtualDisplayDevice 类中,对Mode进行赋值,并且refreshRate为int 的final常量值,也就是60HZ
//VirtualDisplayDevice private static final float REFRESH_RATE = 60.0f;每秒钟 60 帧的屏幕刷新频率,也就是 1000 / 60 ≈ 16.67ms 。
在没有 VSYNC 同步信号脉冲情况下 : (Jank 为同一帧在屏幕上出现 2 次以上)
时间从0开始,进入第一个16ms:Display显示第0帧,CPU,GPU处理第一帧。时间进入第二个16ms:因为早在上一个16ms时间内,第1帧已经由CPU,GPU处理完毕。Display可以正常显示第1帧。但在当前的 16ms期间,CPU和GPU却并未及时绘制第2帧数据(注意前面的空白区),而是在本周期快结束时,CPU/GPU才去处理第2帧数据。时间进入第三个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank)。所以从Android 4.1Jelly Bean开始,Project Buffer引入了VSYNC,系统在收到VSync pulse后,将马上开始下一帧的渲染。结果如下图所示:
所以关键点在于 CPU 在每个 VSYNC 信号到来时,必须开始着手处理下一帧,然后交由 GPU 处理,最后再由屏幕(display)显示出来,整个流水线作业的步调一致是保证显示系统流畅的基础。并且程序的大多数 UI 操作都要在 16.67ms内执行完成。
如果在主线程的某个操作耗时 24ms ,那么用户在 32ms 内看到的都将会是同一帧画面 所以在主线程中的耗时操作会影响 ui 的流畅度。
更多Project Butter 可以参考 Android Project Butter分析 , 我们下一篇文章将会分析 CPU GPU 以及屏幕的渲染机制。
回归正文,我们继续看DisplayEventReceiver,当它订阅了下一次VSYNC 信号后,VSYNC 信号到来时,就会回调 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); } //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); } }接收到 VSYNC 信号订阅事件后,回调 onVsync()方法,因为 UI 事件的处理一定要在主线程,所以FrameDisplayEventReceiver 实现 Runnable 的 run() 方法,通过 Handler 发送异步消息,由 run() 执行 UI 事件。
//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); } }到这里我们就可以回归到 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();兜了一圈回到了ViewRootImpl,上面大致说明了我们当前的遍历操作,对下一帧的准备工作是,当我们ViewRootImpl遍历结束,将绘制结果交给屏幕以便显示。 如果迟迟交不出View的绘制结果,那么屏幕将会一直显示当前画面。
Android 4.1 以前一直沿用double-buffer 双缓冲技术,也就是两块显示 Buffer,back buffer用于CPU/GPU下一帧的绘制准备,另一块 frame buffer 则用于屏幕显示,当back buffer准备就绪后,它们才进行交换。
但是如果我们的准备时间太久,有可能因为 主线程耗时阻塞,xml 布局文件层次过多冗余臃肿,绘制操作不当(onDraw中频繁创建对象) ,导致back buffer 缓冲数据迟迟没有准备好,那么屏幕上就会一直显示 frame buffer ,造成卡顿视觉。
当CPU / GPC 准备B Buffer 内容时间过长,导致第一个VSYNC信号到来时不能交付 back Buffer ,那么屏幕上显示的还是之前的那块 PRE Buffer , 并且 B Buffer 内容准备完成后,还需要等待下一个 VSYNC 信号才能交付。因为在第二个 VSYNC 信号到来时,两块 Buffer 都已经被占用(一块用来显示 ,一块被 B Buffer 准备工作持有),所以当下一次绘制内容也存在延迟的情况也会造成连锁卡顿。(同一帧画面显示 2 次及以上)解决上面问题的办法就是引入第三块 Buffer , 在渲染 B 超时而且 Buffer A 又用于屏幕显示时,可以用第三块 Buffer 来进行C 的准备工作,这样便减少了后面的一次 Jank 发生。
系统大部分情况下都会使用两个Buffer 来完成显示,只有在某一帧的处理时间超过 2 次 VSYNC 信号周期才会使用第三块 Buffer。
最后补上一张本文整个流程的大致时序图,下篇文章将会继续探索CPU / GPU 渲染相关知识。
查看大图
Android Project Butter分析 android屏幕刷新显示机制
Android Choreographer 源码分析/
