1、基本概念
Android屏幕的刷新包括3个重要的部分:CPU、GPU、屏幕。
- 1、CPU:负责View的测量、布局、绘制,将操作完成的数据提交给GPU。
- 2、GPU:负责对CPU提交的数据进行渲染,将渲染完成的数据写入buffer中。
- 3、屏幕:每隔16.6ms从buffer中取出并展示在屏幕上。屏幕从buffer中读取数据的频率是固定的,但是CPU/GPU将数据写入缓存却是随机的。
既然屏幕读取频率是固定的,CPU、GPU写数据是随机的,那么当屏幕读取数据的时候,CPU、GPU还在写数据,会导致一部分数据被复写,buffer里面的数据会来自不同帧,就会出现画面的“撕裂”。
那么有什么方法可以减少画面的“撕裂”呢?
2、双缓冲机制
为了解决画面“撕裂”问题的,Android引入了双缓冲机制。从原先的单缓存变成了Frame Buffer和Back Buffer
。GPU只负责向Back Buffer中写入数据,屏幕只从Frame Buffer中取数据,GPU会定期交换Back Buffer和Frame Buffer,交换的频率是60次/秒,这也就和屏幕的刷新频率保持一致。
3、丢帧是怎么发生的
3.1、布局嵌套过深、过多的View需要刷新或CPU性能较差
引入了双缓存就一定没有问题了吗?
当布局比较复杂或者CPU性能较差时,我们并不能保证绘制能在16.6ms内完成,这就有可能会导致到了Back Buffer和Frame Buffer交换的时间点时,GPU还在向Back Buffer中写数据,如果强行交换,那么这一帧的数据肯定是不完整的。
为此系统做了如下处理:
在GPU向Back Buffer中写数据时将Back Buffer锁住,不交换Back Buffer和Frame Buffer,让屏幕依然显示上一帧的内容,在下次交换的时间点到来时再进行交换。
这样就会导致丢帧。所以我们在开发中尽量较少布局的层级嵌套,减少不必要的View的刷新,避免过多对象的创建。
3.2、如果View的绘制保证在16.6ms内完成,就一定能避免丢帧吗?
即使View绘制耗时较少,但是如果在下次交换的时间点马上到来的时候,才开始绘制,这也会导致在交换时Back Buffer处于锁定状态,导致丢帧。
Vsync是屏幕刷新的信号,每隔16.6ms由底层发出。
如果能保证每次屏幕刷新信号到来的时候,就开始绘制,是不是就能解决这个问题呢?
image.png
Android系统开始引入
Choreographer
这个类来保证Vsync和绘制的同步。从上图中可以看出:蓝色(屏幕)区域中的数字
0、1、2、3、4
就代表屏幕上一帧帧的数据,它是和绘制部分(绿色区域)中的数字对应的。每次Vsync信号到来的时候绘制的是下一帧的数据,绘制完成后并不会立马显示在屏幕上,只是将数据写入到Back Buffer中,等下一次Vsync信号到来时,GPU交换Back Buffer和Frame Buffer将之前绘制的数据显示在屏幕上,与此同时开始绘制下一帧的数据。
4、Choreographer
源码分析的难点就是在代码的海洋中找不到切入点,既然本文是在讲Android屏幕的刷新,那咱们就从View的invalidate()
开始。View的invalidate()具体源码这里不做分析,只大致说下流程:
- 1、View.invalidate()会调用其父类ViewGroup的invalidateChild()
- 2、ViewGroup.invalidateChild()中循环查找parent,并调用parent.invalidateChildInParent()。
- 3、最终会执行View树顶层的DecorView的parent ViewRootImpl.invalidateChildInParent()
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
//省略....
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
//省略...
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
}
方法最终执行到了ViewRootImpl.scheduleTraversals();
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
终于到了关键的代码了,先说下这段代码的作用:向Choreographer发送一个信号告诉它我要刷新,然后等待下一个Vsync信号到来的时候去刷新View。
这部分有几个重点:
- 1、mTraversalScheduled默认为false,进入判断后被置成true,那这个方法是不是就只能执行一次啊,肯定不是,下面我们再进行分析。
- 2、
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
添加同步消息屏障,稍后分析。 - 3、
mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
向Choreographer发送消息。
4.1、Choreographer.postCallback()
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
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);
}
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;
//以当前时间戳放进callbackType=Choreographer.CALLBACK_TRAVERSAL的mCallbackQueues队列中
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
//由于postCallbackDelayed()传入的delayMillis=0,则dueTime = now
if (dueTime <= now) {
//如果没有消息延时,则直接执行
scheduleFrameLocked(now);
} else {
//消息延时,最终依然会执行到scheduleFrameLocked()
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
//设置Message为异步消息
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
Choreographer##scheduleFrameLocked()
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//判断是否在主线程
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
//非主线程中,发消息到主线程最终会执行scheduleVsyncLocked()
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
//设置消息为异步消息
msg.setAsynchronous(true);
//插到消息队列头部,可以理解为设置最高优先级,保证该消息能尽快执行
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
//省略....
}
}
}
Choreographer##scheduleVsyncLocked()
Choreographer.scheduleVsyncLocked()最终会调用DisplayEventReceiver.scheduleVsync()
/**
* Schedules a single vertical sync pulse to be delivered when the next
* display frame begins.
*/
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 {
nativeScheduleVsync(mReceiverPtr);
}
}
跟到这里调用native方法,跟不下去了,哈哈。既然这样,我们换种思路,还记得我们在ViewRootImpl.scheduleTraversals()
向队列中存入了一个action等待执行,那么我们就找下这个action是在哪里取出来的。
Choreographer的内部类CallbackQueue
private final class CallbackQueue {
private CallbackRecord mHead;
public boolean hasDueCallbacksLocked(long now) {
return mHead != null && mHead.dueTime <= now;
}
//取出操作
public CallbackRecord extractDueCallbacksLocked(long now) {
CallbackRecord callbacks = mHead;
if (callbacks == null || callbacks.dueTime > now) {
return null;
}
CallbackRecord last = callbacks;
CallbackRecord next = last.next;
while (next != null) {
if (next.dueTime > now) {
last.next = null;
break;
}
last = next;
next = next.next;
}
mHead = next;
return callbacks;
}
//省略...
}
跟踪发现CallbackQueue.extractDueCallbacksLocked()
在Choreographer##doCallbacks()
中调用
Choreographer##doCallbacks()
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
// We use "now" to determine when callbacks become due because it's possible
// for earlier processing phases in a frame to post callbacks that should run
// in a following phase, such as an input event that causes an animation to start.
final long now = System.nanoTime();
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
//省略...
}
Choreographer##doCallbacks()
在Choreographer##doFrame()
中调用
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
//省略....
try {
//省略...
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
//省略....
}
跟踪发现Choreographer##doFrame()
被调用地方有几个,我们主要看下FrameDisplayEventReceiver
类中的调用。
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);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
//省略...
mTimestampNanos = timestampNanos;
mFrame = frame;
//组织Message时将callBack设置为this,在从MessageQueue中取出消息时会优先执行Message中的callBack,否则去执行Handler中的callBack,如果都未设置callBack则去执行handleMessage()
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);
}
}
FrameDisplayEventReceiver类是继承DisplayEventReceiver接收底层Vsync信号开始处理UI的过程。Vsync是由SurfaceFlinger每隔16.6ms发送一次,当Vsync信号到来时会回调FrameDisplayEventReceiver的onVsync()方法发送消息到主线程,去执行run()中的doFrame()。
也就是说onVsync()
方法是每隔16.6ms收到Vsync信号时回调的,那么问题来了:我们怎么知道Vsync信号来了?肯定是我们在某处注册了一个监听。还记得之前那个native方法吗?
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 {
nativeScheduleVsync(mReceiverPtr);
}
}
这里我们就可以大胆猜测一下:这个native方法内部应该就是实现了注册监听的功能。这样才能做到SurfaceFlinger定时发送来Vsync信号时,回调onVsync()
方法,然后发消息到主线程去执行doFrame()
方法从mCallbackQueues
中取出callbackType为Choreographer.CALLBACK_TRAVERSAL的队列,以时间戳从队列中取出先前调用ViewRootImpl.scheduleTraversals()
中存入的Runnable去执行,最终会调用ViewRootImpl.doTraversal()
。
ViewRootImpl##doTraversal()
void doTraversal() {
//mTraversalScheduled 置成false
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步消息屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
ViewRootImpl##performTraversals()
private void performTraversals() {
if (...) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
performDraw();
}
}
performTraversals()
中就是对这个View树的测量、布局、绘制。
整个过程还是比较复杂了,这里做一下梳理:
- 1、调用View.invalidate()刷新布局时会调用ViewRootImpl.scheduleTraversals()通过Choreographer的postCallBack()将View树的测量、布局、绘制操作封装到Runnable中,并以当前时间戳将其存入mCallbackQueues队列中(这个队列类似于MessageQueue,以时间戳进行排序),等待执行。
- 2、在Choreographer中注册一个信号的监听,当SurfaceFlinger每隔16.6ms发送Vsync信号时就回调FrameDisplayEventReceiver的onVsync()方法,在onVsync()中发送一条消息到主线程中去执行doFrame()
- 3、doFrame()中取出队列中先前存入的View树的测量、布局、绘制操作封装的Runnable去执行。
- 4、所以说,当我们调用invalidate、requestLayout等刷新操作时,并不是马上会执行刷新的操作,而是通过ViewRootImpl 的 scheduleTraversals() 先向底层注册一个屏幕刷新的监听,然后等下一个屏幕刷新信号到来的时候才会调用performTraversals() 遍历绘制 View 树来执行这些刷新操作
5、过滤同一帧内多次刷新操作
问:如果同一帧内(16.6ms内),我们多次调用了invalidate或requestLayout,是不是就在底层注册了多个监听,当下次屏幕刷新信号到来的时候,然后执行多次perforTraversals(),多次遍历View树进行View的绘制呢?
答:为啥要多次遍历View树,即使一帧内有n个View需要刷新,我们也只需要遍历一次View树(毕竟这些View都在View树内)就可以完成n个View的刷新操作。google工程师也是这么做的,下面看下代码。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//....
}
}
mTraversalScheduled默认false,在第一个View刷新请求时,会进入判断在底层注册一个监听并把mTraversalScheduled置成true,那么后面再次过来的刷新请求就不再执行,那mTraversalScheduled又是在哪里被重置的呢。
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//.....
}
}
}
在doTraversal()
中就将mTraversalScheduled重置成了false,也就是说当我们调用了一次scheduleTraversals()
之后,直到下一个屏幕刷新信号来的时候,doTraversal()
被取出来执行。在这期间内重复调用scheduleTraversals()
都会被过滤掉的。
6、同步屏障消息
MessageQueue.postSyncBarrier()
该方法的作用是发送同步屏障。消息的同步屏障可以理解为拦截同步消息的执行。熟悉Handler消息机制的同学都知道,在主线程中Looper循环调用
MessageQueue.next()
中取出Message
去执行,只有当Message执行完成后才会取下一个。当调用MessageQueue.next()
取消息时,发现队头是一个同步屏障的消息时,就会遍历队列只寻找设置了异步标志的消息,如果找到这个异步消息就取出来执行,否则就让next()
方法进入阻塞状态。如果next()
陷入了阻塞,那么后面的同步消息都被拦截住了,直到这个同步的屏障消息被移除,才会处理后面的同步消息。
添加同步屏障的消息就是为了让部分优先级较高的消息能更快的执行。主线程中不可能只处理屏幕刷新,如果在MessageQueue中处理屏幕刷新的Message前面有很多其他的Message,那么在屏幕刷新信号到来的时候,我们并不能很快执行到屏幕刷新的Message,在16.6ms过去了,下一帧的屏幕刷新信号到来了,我们还没执行完View的绘制,这依然会导致丢帧。
在View调用invalidate()
请求刷新时,会调用ViewRootImpl.scheduleTraversals()
向MessageQueue
中发送一个同步屏障消息,即使View树遍历的Message前很多其他的同步消息,当屏幕刷新信号到来时也会优先执行View树的遍历(doTraversal()中移除同步屏障消息,不再拦截其他同步消息),保证屏幕刷新信号和View树的遍历的同步。
那么,有了同步屏障消息的控制就能保证每次一接收到屏幕刷新信号就第一时间处理遍历绘制 View 树的工作么?
只能说,同步屏障是尽可能去做到,但并不能保证一定可以第一时间处理。因为,同步屏障是在 scheduleTraversals() 被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在 scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行。
总结
- 1、由于屏幕从buffer中读取数据的频率是固定,但是CPU、GPU向buffer中写入数据却是随机了,这就有可能导致画面的撕裂,为此引入了双缓冲机制。
- 2、如果在屏幕刷新信号到来时,CPU、GPU还在向Back Buffer中写数据,那么Back Buffer将被锁定,等待下次的屏幕刷新信号到来时GPU再切换Back Buffer为Frame Buffer,这也就导致了丢帧。
- 3、即使View的绘制很快,如果不能保证屏幕刷新信号和绘制的同步,在下一帧屏幕刷新信号快来时才开始执行View的绘制也会导致BackBuffer的被锁定,最终导致丢帧。
- 4、Choreographer就是为了尽可能保证屏幕刷新信号和View绘制的同步。在View调用
invalidate
或者requestLayout
时就调用ViewRootImpl.scheduleTraversals()
向MessageQueue
中发送一个同步屏障的消息,并注册一个信号监听,将View的绘制封装成Runnable存入队列中等待下一帧屏幕刷新信号到来时,onVsync()
就会被回调发送异步消息到主线程(由于有同步屏障消息,异步消息优先执行),调用doFrame()
取出之前存入Runnable去执行View的绘制。
网友评论