在上一篇 View的工作流程 的博客中,分析了ViewRootImpl
类中应用窗口 measure,layout 和 draw 的过程。今天这篇文章探索从ViewRootImpl 到屏幕的刷新之间的渊源。
//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的绘制流程正式开始。
}
针对以下 2 个要点分析
① postSyncBarrier : Handler 的同步屏障
简单来说,同步屏障的作用是可以拦截 Looper 对同步消息的获取和分发,因为 Handler 消息机制中,Looper 会不断的从 MessageQueue 中取出 Message。加入同步屏障之后,Looper 只会获取和处理异步消息,如果没有异步消息那么就会进入阻塞状态。
这么做的原因是 View 的绘制和屏幕刷新优先级肯定是最高的(也就是 VIP ,防止卡顿), 除了对View绘制渲染的处理操作可以优先处理(设置为异步消息),其它的 Message 都可以放置一边。保障弱势群体的权益。
① Choreographer : 编舞者。(系统工程师认为 View的渲染绘制就是指尖上的艺术,每一次交互都是一场视觉盛宴,创作过程中增添几分意境)
Choreographer 是 Jelly Bean(Android 4.1)中黄油计划(Project Butter)引入的产物,包括 :
在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 次以上)
所以从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 ,造成卡顿视觉。
解决上面问题的办法就是引入第三块 Buffer , 在渲染 B 超时而且 Buffer A 又用于屏幕显示时,可以用第三块 Buffer 来进行C 的准备工作,这样便减少了后面的一次 Jank 发生。
系统大部分情况下都会使用两个Buffer 来完成显示,只有在某一帧的处理时间超过 2 次 VSYNC 信号周期才会使用第三块 Buffer。
最后补上一张本文整个流程的大致时序图,下篇文章将会继续探索CPU / GPU 渲染相关知识。
Android Project Butter分析
android屏幕刷新显示机制
Android Choreographer 源码分析/