Potato
Potato copied to clipboard
Android Screen refresh mechanism
Android Screen refresh mechanism
[TOC]
这篇文章在 View 的绘制基础上,依旧从 ViewRootImpl 开始,探索与屏幕刷新之间的渊源。
View 的工作流程
void scheduleTraversals() {
if (!mTraversalScheduled) {
// 同一帧不会多次调用遍历
mTraversalScheduled = true;
// handler 的同步屏障,发送一条异步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 入口
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
上述有两个地方:
-
postSyncBarrier: handler 中的同步屏障
一般来说,同步屏障的作用时可以拦截 Looper 对同步消息的获取和分发。因为 handler 消息机制中 MessageQueue 会不断的取出 Message。加入同步屏障之后,Looper 只有获取和处理异步消息,如果没有异步消息,马么就会进入阻塞状态。
原因子啊雨 View 的绘制和屏幕刷新时优先级最高的事情(防止卡顿),除了对 View 绘制的处理操作可以优先处理(异步消息),其他的 Message 都可以慢处理。
-
Choreographer:编排。协调动画、输入和绘图的时间。从显示子系统接收定时脉冲(例如垂直同步),然后调度作为呈现下一个显示帧的一部分而发生的工作。
Choreographer
这个类包括三部分:
- Choreographer:负责统一动画,输入和绘制时机
- VSYNC:垂直同步信号
- Triple Buffer:第三块绘制 Buffer,减少显示内容的延迟
在 ViewRootImpl 的上述方法中,choreographer.postCallback() 方法执行,遍历绘制view。
// 执行要在下一帧上运行的回调
// 执行一次自动移除
@TestApi
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
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;
//根据时间将 action 添加到 mCallbackQueue 的队列中
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
//设置异步延迟消息 ,过dueTime后执行(无视同步屏障)
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on 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()) {
// UI 线程
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
//将异步消息放置Handler队列的最前面,当前是最高优先级。
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
}
}
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,当下一个脉冲到来时会回调dispatchVsync方法
nativeScheduleVsync(mReceiverPtr);
}
}
VSYNC
VSYNC 的全称是 Vertical Synchronization ,即垂直同步。
由于人眼与大脑之间的协作一般情况无法感知超过60FPS的画面更新。如果所看到画面的帧率高于12帧的时候,就会认为是连贯的,达到24帧便是流畅的体验,这也就是胶片电影的播放速度(24FPS)
对于屏幕显示,游戏体验来说,如果能整体平稳的达到60FPS,画面每秒更新60次,也就是16.67ms刷新一次,绝大部分人视觉体验都会觉得非常流畅如丝般顺滑。
public float getRefreshRate() {
synchronized (this) {
updateDisplayInfoLocked();
return mDisplayInfo.getMode().getRefreshRate();
}
}
每秒钟 60 帧的屏幕刷新频率,也就是 1000 / 60 ≈ 16.67ms 。
在没有 VSYNC 同步信号脉冲情况下 : (Jank 为同一帧在屏幕上出现 2 次以上)
兜了一圈回到了ViewRootImpl
,大致说明了我们当前的遍历操作,对下一帧的准备工作是,当我们ViewRootImpl
遍历结束,将绘制结果交给屏幕以便显示。
如果迟迟交不出View的绘制结果,那么屏幕将会一直显示当前画面。
onVsync是底层回调回来的,那也就是每16.6ms,底层会发出一个屏幕刷新的信号,然后会回调到onVsync方法之中,但是有一点很奇怪,底层怎么知道上层是哪个app需要这个信号来刷新呢,结合日常开发,要实现这种一般使用观察者模式,将app本身注册下去,但是好像也没有看到哪里有往底层注册的方法,对了,再次回到上面的那个native方法,nativeScheduleVsync(mReceiverPtr),那么这个方法大体的作用也就是注册监听了,
同步屏障
当设置了同步屏障之后,next函数将会忽略所有的同步消息,返回异步消息。换句话说就是,设置了同步屏障之后,Handler只会处理异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息
总结
- View的刷新请求都会走到ViewRootImpl的scheduleTraversals() 中,通过Runnable的形式形成Message放进队列中,并且发送同步屏障,拦截所以同步消息,处理异步消息,以此尽快的保证执行刷新任务
- 常说的每隔16.6ms刷新一次屏幕实际上是,底层会以这个频率来切换每一帧的画面,只有当View发起了刷新请求时,App才会想底层去注册监听下一个屏幕的刷新信号,并且才能受到下一次信号到来的通知回调onVsync
- App负责计算屏幕刷新的数据,但是并非计算完成后就会立即刷新数据,更多的取决于是否到了下一次底层要刷新屏幕的指令回调的时机,所以也就回答了上面的问题,每次指令到达时才会去刷新数据,尽可能的保证刷新数据的任务有足够16.6ms的时间。
- 造成屏幕丢帧的原因也就很明显了,1.View树绘制的任务时长大于了16.6ms,此时下一个信号来临,导致丢帧 2.虽然采用了同步屏障的方法来保证足够View绘制任务的时间,但是如果同步屏障之间的Message耗时过长,也导致遍历绘制 View 树的工作迟迟不能开始,从而超过了 16.6 ms 底层切换下一帧画面的时机,这也就是主线程不要做耗时操作的原因了