这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战
Android界面越来越复杂,很多时候页面会出现掉帧什么的,今天特意研究一下屏幕刷新的原理,方便以后复习
屏幕刷新机制大致流程
首先应用程序向系统服务申请一块buffer(缓存),系统服务返回buffer,应用拿到buffer之后就可以进行绘制,绘制完之后将buffer提交给系统服务,系统服务将buffer写到屏幕的一块缓存区,屏幕会以一定的帧率刷新,每次刷新的时候,就会从缓存区将图像数据读取显示出来。如果缓存区没有新的数据,就一直用旧的数据,这样屏幕看起来就没有变。
屏幕的图像缓存不止一个,假如只有一个缓存,如果屏幕这边正在读缓存,而系统服务又在写缓存,这有可能导致屏幕显示不正常,如一半显示第一帧图像的画面,另一半显示第二帧图像的画面。如何避免这种问题的发生呢?可以弄多个缓存,屏幕从一块缓存读取数据显示,系统服务向另一块缓存写入数据。如果要显示下一帧图像,将两个缓存换一下即可,即屏幕从缓存2读取显示,系统服务向缓存1写入数据。
vsync(垂直同步机制)是固定频率的脉冲信号,屏幕根据这个信号周期性的刷新,屏幕每次收到这个信号,就从屏幕缓存区读取一帧的图像数据进行显示,而绘制是由应用端(任何时候都有可能)发起的,如果屏幕收到vsync信号,但是这一帧的还没有绘制完,就会显示上一帧的数据,这并不是因为绘制这一帧的时间过长(超过了信号发送周期),只是信号快来的时候才开始绘制,如果频繁的出现的这种情况,用户就会感知屏幕的卡顿,即使绘制时间优化的再好也无济于事,因为这是底层刷新机制的缺陷。
当然系统提供了解决方案,如果绘制和vsync信号同步就好了,每次收到vsync信号时,一方面屏幕获取图像数据刷新界面,另一方面应用开始绘制准备下一帧图像数据。如果优化的好,每一帧图像绘制控制在16ms以内,就可以非常流畅了。
应用层view的重绘一般调用requestLayout触发,这个函数随时都能调用,如何控制只在vsync信号来时触发重绘呢?有一个关键类Choreography(舞蹈指导,编舞),它最大的作用就是你往里面发送一个消息,这个消息最快也要等到下一个vsync信号来的时候触发。
Choreography 原理分析
进入ViewRootImpl类中的requestLayout 方法
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
复制代码
进入scheduleTraversals 方法
@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
复制代码
scheduleTraversals方法里面,主要做了两件事:
- 向线程的消息队列中加入了syncBarrier。
- 往mChoreographer的mCallbackQueue数组插入了一个callback(需要执行的相关操作,这里主要是UI绘制)
syncBarrier是一个屏障,将它插入到消息队列后,这个屏障后面的普通消息就不能处理了,等到屏障撤除之后才能处理。但是这个屏障对异步消息是没有影响的。主要是有些类型的消息非常紧急,需要马上处理。如果普通消息太多,容易耽误事(影响紧急消息的执行),所以插入了一个屏障,优先处理异步消息。
请求同步Vsync信号,就是一个异步消息,就是请求系统服务SurfaceFlinger在下一次Vsync信号过来时,立即通知我们,我们就可以立即执行mChoreographer的callback数组里面对应callback的相关操作,即UI绘制了
public static Choreographer getInstance() {
return sThreadInstance.get();
}
复制代码
Choreography和ViewRootImpl一起创建的,是通过ThreadLocal存储的,即在不同的线程调用getInstance得到的是不同的Choreography对象
Choreographer要执行的操作就是mTraversalRunnable即TraversalRunnable的run方法,即doTraversal
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
复制代码
进入doTraversal方法
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步消息屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//开始绘制流程
performTraversals();
。。。
}
}
复制代码
进入Choreography的postCallback方法
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
@UnsupportedAppUsage
@TestApi
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);
}
复制代码
进入postCallbackDelayedInternal方法
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;
// 将执行动作放在mCallbackQueue数组中
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
//如果已经到触发时间就注册请求垂直同步信号
scheduleFrameLocked(now);
} else {
// 如果还没有到触发时间,使用handler在发送一个延时的异步消息。
// 这个延时消息会在到触发时间的时候执行
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
复制代码
mCallbackQueue数组里面的每一个元素都是一个callback的单链表,添加callback一方面要根据callback的类型callbackType插到对应的单链表,另一方面要根据callback执行的时间顺序排序,越是马上要执行的callback,越是插入到链表的前面,然后等待被调用。
进入scheduleFrameLocked方法
private void scheduleFrameLocked(long now) {
。。。。
// 如果在Choreography的UI线程中,就直接调用立即安排垂直同步,否则就发送一个消息到UI线程
// 尽快安排请求一个垂直同步
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
。。。
}
复制代码
vsync信号过来的时候,让系统服务SurfaceFlinger第一时间通知我们,我们去执行绘制的相关操作。如果不在Choreography的UI线程,就发送异步消息让UI线程请求同步Vsync信号
关于消息处理的逻辑:
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}
复制代码
FramHandler拿到 whate属性值为MSG_DO_SCHEDULE_CALLBACK的时候会去执行 doScheduleCallback(msg.arg1)方法
void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}
复制代码
如果满足条件的情况下它会调用 scheduleFrameLocked()这个方法。最终都是调用scheduleVsyncLocked方法
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync方法();
}
复制代码
进入scheduleVsync方法
@UnsupportedAppUsage
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 {
// native方法
// 请求同步vsync信号
nativeScheduleVsync(mReceiverPtr);
}
}
复制代码
当下一个vsync信号发生的时候,SurfaceFlinger就会通知我们,就会回调该类的onVsync函数,参数timestampNanos就是vsync的时间戳,该函数里面会发送一个消息到Choreography的工作线程里面去了,这里并不是要切换工作线程,因为onVsync本身就在Choreography的工作线程。这个消息带了时间戳的,表示消息触发的时间,有了这个时间戳,就可以按照时间戳的顺序来处理消息。到时间了,就会去执行run方法,即执行doFrame方法
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource, CONFIG_CHANGED_EVENT_SUPPRESS);
}
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
long now = System.nanoTime();
if (timestampNanos > now) {
Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
+ " ms in the future! Check that graphics HAL is generating vsync "
+ "timestamps using the correct timebase.");
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;
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);
}
}
复制代码
进入doFrame方法
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
//当前时间
startNanos = System.nanoTime();
//当前时间和垂直同步时间
final long jitterNanos = startNanos - frameTimeNanos;
//垂直同步时间和当前时间的差值如果大于一个周期就修正一下
if (jitterNanos >= mFrameIntervalNanos) {
//取插值和始终周期的余数
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
//当前时间减去上一步得到的余数当作最新的始终信号时间
frameTimeNanos = startNanos - lastFrameOffset;
}
//垂直同步时间上一次时间还小,就安排下次垂直同步,直接返回
if (frameTimeNanos < mLastFrameTimeNanos) {
scheduleVsyncLocked();
return;
}
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
if (DEBUG_FRAMES) {
final long endNanos = System.nanoTime();
Log.d(TAG, "Frame " + frame + ": Finished, took "
+ (endNanos - startNanos) * 0.000001f + " ms, latency "
+ (startNanos - frameTimeNanos) * 0.000001f + " ms.");
}
}
复制代码
doFrame分为两个阶段:
- 第一个阶段,参数frameTimeNanos表示这一帧的时间戳先计算当前时间和这个时间戳的间隔有多大,间隔越大,表示要延时了,如果延时超过一个周期(mFrameIntervalNanos), 就要计算到底延迟了几个周期,如果延迟周期数(丢帧数,跳过的帧数)达到一个常量SKIPPED_FRAME_WARNING_LIMIT ,就会打印日志"应用在主线程做了太多的事情(耗时操作)"导致绘制延迟,丢帧。
- 第二阶段就是处理callback了,callback有四种类型,每种类型对应一个单链表callbackQueue,给vsync事件分别分发到四种callback,然后执行对应的doCallbacks函数,单链表里面的callback是有时间戳的,只有到了时间的的callback才会回调,extractDueCallbacksLocked从callbackQueue里面取出到了时间的callback,然后在循环里面执行他们的run函数,requestLayout里面的scheduleTraversals函数传的callback是什么?就是准备绘制,mTraversalRunnable的run方法其实调用的是performTraversals,真正的开始执行UI绘制流程。
大致流程
常见的面试问题
丢帧一般是什么原因引起的?
主线程有耗时操作,耽误了view的绘制
Android刷新频率60帧/秒,每隔16ms调onDraw绘制一次?
60帧/秒也是vsync信号的频率,但不一定每次vsync信号都会去绘制,先要应用端主动发起重绘,才会向SurfaceFlinger请求接收vsync信号,这样当vsync信号来的时候,才会真正去绘制。
onDraw执行完之后屏幕会马上刷新么?
不会马上刷新,会等到下一次vsync信号时才会刷新。
如果界面没有重绘,还会每隔16ms刷新屏幕么?
界面没有重绘,应用就不会收的vsync信号,屏幕还是会刷新,画面数据用的是旧的,看起来没什么变化而已
如果屏幕快要刷新的时候才去onDraw绘制会丢帧么?
重绘不会立即执行,而是等到下一次vsync信号来时才开始, 所以什么时候发起重绘影响不大