1.显示系统基础知识
显示系统一般包括CPU、GPU、Display三部分,其中CPU负责计算帧数据,然后把计算好的数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到图像缓冲区buffet里存起来,然后Display(屏幕或显示器)负责把buffer里的数据呈现到屏幕上。
这里涉及几个基础概念:
①屏幕刷新频率
屏幕刷新频率是指一秒内屏幕刷新的次数,即一秒内显示了多少帧图像,单位是赫兹Hz。常见的屏幕刷新频率是60Hz。
注:刷新频率取决于硬件的固定参数,不会变的。
②逐行扫描
显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,这一过程快到人眼无法察觉到变化。
比如常见的刷新率为60 Hz的屏幕,这一过程只需要1000 / 60 ≈ 16ms。
③帧率Frame Rate
帧率指GPU在一秒内绘制操作的帧数,单位fps。Android系统采用60fps,即每秒钟GPU最多绘制60帧画面。
帧率是动态变化的,例如当画面静止时,GPU是没有绘制操作的,屏幕刷新的还是buffer中的数据,即GPU最后操作的帧数据。
④画面撕裂tearing
指一个屏幕内的数据来自2个不同的帧,导致画面出现了撕裂感。
比如屏幕更新到一半的时候用户进程更新了Frame Buffer中的数据,这将导致屏幕上画面的上部分是前一帧的画面,下半部分变成了新的画面,于是就会让用户觉得画面有闪烁感。
解释一下画面撕裂的原因:
单缓冲区时,图像绘制和屏幕读取操作使用的是同一个Frame Buffer,导致屏幕刷新时可能读取到的是不完整的一帧画面。
屏幕刷新频率是固定的,比如每16.6ms从buffer中取出数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器就显示一帧。但由于CPU/GPU写数据是不可控的,可能会出现buffer里有些数据根本没显示出来就被重写了,即buffer里的数据可能是来自不同的帧, 当屏幕刷新时,它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,也就出现了画面撕裂。简单说就是因为单缓冲区时图像绘制和屏幕读取操作使用的是同一个Frame Buffer,造成Display在显示的过程中,buffer内的数据可能会被CPU/GPU修改,因而出现了画面撕裂。
为了解决画面撕裂问题,出现了双缓存。
2.双缓存机制+Vsync
双缓存机制就是让绘制和显示拥有各自的buffer,CPU和GPU共用后缓冲,它们轮流使用后缓冲,而Display单独使用前缓冲。CPU计算数据后交给GPU渲染,然后GPU将完成的一帧图像数据写入到后缓冲中,显示器每次刷新屏幕时都从前缓冲中取数据。这样当屏幕刷新时,前缓冲中的数据并不会发生变化,只有当后缓冲数据准备就绪后,它们才进行交换。如下图:
注意:
虽然引入了双缓存,但如果切换的时间点不对,比如在画面更新到一半时切换,还是会不可避免地产生画面闪烁的异常。那两个buffer的内容什么时候进行交换呢?假如是后缓冲区准备完成一帧数据后就进行交换,那如果此时屏幕还没有完整显示上一帧内容的话,肯定会出问题。所以只能等到屏幕显示完一帧数据后才可以执行交换操作。
屏幕刷新时,当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次循环,此时有一段时间空隙,称为Vertical Blanking Interval(VBI)。这个时间点就是进行缓冲区交换的最佳时间了,因为此时屏幕没有在刷新,也就避免了交换过程中出现画面撕裂的状况。
Android引入VSync机制来确定缓冲交换时间,VSync就是利用VBI时期出现的垂直同步脉冲vertical sync pulse来保证双缓冲在最佳时间点进行交换(是指各自的内存地址,可以认为该操作是瞬间完成)。也就是由底层模拟VSync信号一直固定发出,当用户进程接到Vsync信号时就开始渲染处理,当VSync信号到来且缓冲区数据准备完毕后,就会进行缓存交换。
注意:VSync不仅控制了后缓冲区和前缓冲区的数据交换,还控制了CPU何时开始进行绘制计算。
那么有人就会说:真正解决画面撕裂问题的是VSync,而不是双缓冲,那是不是不要双缓冲只要VSync也可以?
假设只有VSync,比如现在屏幕正在渲染数据,而CPU在等VSync信号,屏幕将数据渲染完毕后,发送VSync信号,CPU收到信号后就去计算数据,计算完后才会写入帧缓冲,那么在CPU计算数据这段时间内屏幕干什么呢?它只能接着刷新帧缓冲区的数据,由于CPU还没有将新数据计算完毕刷入前缓冲,所以显示的还是上一帧的数据,这样就会卡顿。
所以,有双缓冲的情况下,CPU使用后缓冲计算数据,屏幕使用前缓冲渲染数据,两者可以同时工作,计算一个就可以渲染一个,典型的生产者消费者模式,只不过使用VSync信号来进行数据的交换;而没有双缓冲的情况下,两者需要排队使用帧缓冲,不能同时工作,就变成了我等着你计算,你计算完了等着我渲染,VSync此时的作用就是进行排队,这样会大大增加卡顿。因此,VSync真正解决了撕裂问题,而双缓冲优化了卡顿问题。
3.Android屏幕刷新机制
首先大致说一下Android屏幕绘制流程:
①任何一个View都是依附于Window的,一个Window对应一个Surface;
②View的measure、layout、draw等均是计算数据,这些是CPU干的事。CPU把这些事干好后,再经过一系列计算将数据转交给GPU;
③GPU将数据栅格化后,就交给SurfaceFlinger;
④SurfaceFlinger将多个Surface数据合并处理后,就放入后缓冲区;
⑤屏幕以固定频率从前缓冲区拿出数据渲染,渲染完毕后发送VSync,此时前后缓冲区数据交换,屏幕绘制下一帧。
这是建立在开启硬件加速的情况下的(Android 4.0默认开启了硬件加速),如果没有硬件加速,就需要去掉GPU部分,可以简单理解为CPU直接将数据转交给SurfaceFlinger。
从这个过程可以看出数据的传递流程:CPU -> GPU -> Display,而且CPU和GPU是排队工作的(它俩共用一个缓冲区),它俩和屏幕并行工作(屏幕单独用一个缓冲区)。
现在分不同情况详细分析一下屏幕绘制流程:
1)没有VSync的绘制过程
在Android4.1之前,屏幕刷新遵循双缓存机制。
Display一行看作是前缓冲,GPU和CPU两行叠加起来看作是后缓冲(因为它俩排队使用),将VSync线隔离开的竖行看作一个帧。Display表示显示设备,上面的数字表示图像帧序号,GPU方块表示GPU正在准备数据,CPU方块表示CPU正在准备数据。
以时间顺序看一下整个过程:
①在第一个时间周期(即两个VSync之间的间隔)Display正常渲染第0帧画面,此时CPU已经开始准备第1帧数据,GPU正好在CPU准备好后开始处理且在第一个周期内完成;
②因为第1帧数据渲染及时,Display在第0帧显示完成后,缓存进行交换,因此Display正常显示第1帧。但是第2帧开始处理,CPU/GPU并不是在Vsyn一到来时就立马进行处理,而是由于某种原因CPU处理第2帧数据比较晚,GPU在CPU处理完成后再去处理,导致GPU处理完成时已经超过了第二个时间周期。
③由于Display刷新率是固定的,第2个VSync来时,第2帧数据还没有准备就绪,缓存就无法进行交换,所以显示的还是第1帧,就产生了Jank丢帧现象(丢帧、掉帧表示这一帧延迟显示)。
④当第2帧数据准备完成后,它并不能马上被显示,而是要等待下一个VSync到来时才进行缓存交换再显示。
所以,整个过程中屏幕多显示了一次第1帧。直接原因就是第2帧的CPU/GPU计算没能在VSync信号到来前完成 。究其根本会发现第2帧CPU/GPU计算不是在第1个Vsyn一到来时就立马进行处理,而是直到第2个VSync快来前才开始处理,这样就导致第2帧的CPU/GPU计算没能在第2个VSync信号到来前完成计算。
由于双缓存的交换是在Vsyn到来时进行的,交换后屏幕会取Frame buffer内的新数据,此时后缓冲区就可以立即供CPU、GPU准备下一帧数据了。所以,如果Vsync一到来时CPU/GPU立马就开始操作的话,是有完整的16.6ms的,这样基本会避免jank的出现了(除非CPU/GPU计算超过了16.6ms)。
那如何让CPU/GPU计算在Vsyn一到来时就进行呢?Android 4.1引入了黄油计划(VSYNC),上层开始接收VSYNC(Choreographer),并且加入了三缓冲。
2)drawing with VSync
为了优化显示性能,Google在Android 4.1系统中对Display系统进行了重构,实现了Project Butter(黄油工程):系统在收到VSync pulse后,将马上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU就立刻开始计算然后把数据写入buffer。
有了VSync后,CPU总是在指定的地方开始。即CPU/GPU会根据VSYNC信号同步处理数据,这样就可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。
但是这样并不能完全避免jank问题,只能优化卡顿。比如界面比较复杂,CPU/GPU的处理时间较长,超过了16.6ms:
由于CPU/GPU处理耗时过长,超过了16.7ms,当第一个VSync信号到来时,缓冲区B中的数据还未准备完毕,Display未能完成交换只能继续显示缓冲区A中的内容。此时缓冲区A、B分别被Display和GPU占用了,CPU在第二个VSync时无法开始准备下一帧数据而只能空闲运行,当下一个VSync信号来临时,Display与B完成缓冲区交换,CPU才能继续处理下一帧数据,导致的结果就是相当于把屏幕的刷新率降低了。因为在第一个周期时原本应该显示第二帧的又多显示了第一帧。究其原因就是因为两个Buffer都被占用,CPU无法准备下一帧的数据。
分析一下整个过程:
①在第一帧里,GPU墨迹了半天没有搞完,直到第二帧里GPU还在处理B帧,导致缓存没能交换,因此Display显示的还是第一帧的数据,即A帧被重复显示,出现了jank卡顿。
并且会发现在第一个VSync信号过来后,CPU什么都没做,这是因为GPU占着后缓冲(那个绿色的长B块),当B帧完成后,又因为缺乏VSync pulse信号,CPU还是不能开始操作,只能等待下一个VSync信号到来时才能开始。于是在这一过程中,有一大段时间是被浪费的。
②当下一个VSync到来时,缓存进行交换,显示屏显示B帧,CPU终于拿到了后缓冲的使用权,CPU马上开始执行计算A帧数据的操作,这时看起来是正常的,只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了……如此循环反复,便出现了越来越多的Jank。
在这个过程中,会发现有大量的时间是浪费的,比如第2个16ms内、第4个16ms内……这些时间的VSync到来时,CPU/GPU按道理是应该立即进行操作的,但实际却没有。那为什么CPU不能在第二个16ms处理绘制工作呢?这是因为只有两个buffer,Dispaly使用前缓冲,CPU和GPU一起使用后缓冲(这就意味着它们只能轮流使用后缓冲)。此时Back buffer正在被GPU用来处理B帧的数据,Frame buffer的内容用于Display的显示,这样两个buffer都被占用了,CPU就无法准备下一帧的数据。
为了解决这个问题,可以再提供一个buffer,CPU、GPU和Display都能使用各自的buffer工作,互不影响。因此出现了三缓存。
3)三缓存
三缓存就是在双缓冲机制基础上增加一个Graphic Buffer缓冲区,这样可以最大限度的利用空闲时间。缺点是多使用的一个Graphic Buffer所占用的内存。
在第一个VSync到来时,GPU还占用B缓冲区处理数据,所以Display只能显示A缓冲区的老数据。但此时C缓冲区是空闲的,CPU就占用C缓冲区开始计算数据;当第二个VSync到来时,B缓冲区数据处理完毕,Display与GPU缓冲区交换(相当于Display占用B,GPU占用A),因此Display切换到B缓冲区并显示,与此同时CPU处理完C的数据后会与GPU交换,GPU处理C缓冲区,CPU继续使用A缓冲区计算数据;当第三个VSync到来时,Display切换到C缓冲区(因为在第二个周期内GPU处理好了C缓冲),A缓冲被GPU占用,CPU使用B缓冲,如此循环,这样除了第一帧不可避免地产生Jank外,后续的帧显示效果都比较理想。
分析一下整个过程:
①第一个Jank是不可避免的。但是在第二个16ms 时间段,CPU/GPU使用第三个Buffer完成C帧的计算,虽然还是会多显示一次A帧,但后续显示就比较顺畅了,有效避免Jank的进一步加剧。
②注意在第3段中,A帧的计算已完成,但是在第4个VSync来的时候才显示,如果是双缓冲,那在第三个VSync就可以显示了。
通过引入三缓冲,当接到VSync脉冲时Display Buffer可以选择Frame Buffer和Graphic Buffer中已经准备好的数据进行交换。
三缓冲虽然不能完全避免卡顿问题,但可以大幅优化卡顿问题,尤其是避免连续卡顿。
注意:Buffer并不是越多越好,Buffer正常还是两个,当出现Jank后三个足以。
三缓冲有效利用了等待VSync的时间,减少了jank,但同时也带来了延迟、耗资源。 所以系统并非一直开启三缓冲,要想真正解决问题,还需要在CPU层对数据尽量优化,从而减小CPU和GPU的计算量,比如View尽量扁平化,少嵌套,少在UI线程做耗时操作等。
以上就是Android屏幕刷新的原理了。
4.Choreographer [ˌkɒriˈɒɡrəfə(r)]
Choreographer是Android4.1新增的机制,用于配合系统的VSync信号。Choreographer可以接收系统的VSync信号,统一管理应用的输入、动画和绘制等任务的执行时机。在收到VSync pulse后马上开始下一帧的渲染,即一旦收到VSync通知,CPU和GPU就立刻开始计算,然后把数据写入后缓冲。
Choreographer意为舞蹈编导、编舞者。Android中指对CPU/GPU绘制的指导:收到VSync信号后立马开始绘制,保证了绘制拥有完整的16.6ms,避免绘制的随机性。
在Android中,Choreographer是一个Java类,包路径为android.view.Choreographer,它的类注释是:协调动画、输入和绘图的计时。
一般通过Choreographer监控应用的帧率。通常应用层不会直接使用Choreographer,而是使用更高级的API,例如动画和View绘制相关的ValueAnimator.start()、View.invalidate()等。
Choreographer可以帮助理解每帧运行的原理,也可加深对Handler机制、View绘制流程的理解,这样再去做UI优化、卡顿优化,思路会更清晰。
Android中所有UI的变化都会走到ViewRootImpl的scheduleTraversals()方法。比如Activity启动时,走完onResume方法后会进行window的添加,在window添加过程中会调用ViewRootImpl的setView()方法,setView()会调用requestLayout()请求绘制布局,requestLayout()内部就会走到scheduleTraversals()方法,最后会走到performTraversals()方法,接着就到了最熟悉的测量、布局、绘制三大流程了。另外,当使用 ValueAnimator.start()、View.invalidate()时最后也会走到ViewRootImpl的scheduleTraversals()方法(View.invalidate()内会循环获取ViewParent直到ViewRootImpl的invalidateChildInParent()方法,然后走到scheduleTraversals() )。
那scheduleTraversals()到performTraversals()中间经历了什么呢?是立刻执行吗?答案显然是否定的。根据上面的介绍,在VSync信号到来时才会执行绘制,即performTraversals()方法。
下面从源码角度看看这个过程是如何实现的。
①Choreographer分析入口
ViewRootImpl.java:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true; //此字段保证同一时间多次更改只会刷新一次,例如TextView连续两次setText(),也只会走一次绘制流程
mTraversalBarrier = mHandler.getLooper() .getQueue().postSyncBarrier(); //添加同步屏障,屏蔽同步消息,保证VSync到来时立即执行绘制
mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable,null); //mTraversalRunnable是TraversalRunnable实例,最终走到run(),也即doTraversal()方法
…
}
}
其中mTraversalRunnable是TraversalRunnable的实例:
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
TraversalRunnable的定义如下:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
在doTraversal方法中开始view绘制三大流程:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue(). removeSyncBarrier(mTraversalBarrier);
...
performTraversals(); //开始三大绘制流程
...
}
}
主要有以下逻辑:
1)首先使用mTraversalScheduled字段保证同时间多次更改只会刷新一次,例如TextView连续两次setText(),也只会走一次绘制流程。
2)然后把当前线程的消息队列Queue添加了同步屏障,这样就屏蔽了正常的同步消息,保证VSync到来后立即执行绘制,而不需要等前面的同步消息。
3)调用mChoreographer.postCallback()方法发送一个会在下一帧执行的回调,即在下一个VSync到来时会执行TraversalRunnable-->doTraversal()--->performTraversals()-->绘制流程。
②Choreographer实例创建
接下来,重点分析Choreographer。
它的实例mChoreographer是在ViewRootImpl的构造方法内使用Choreographer.getInstance()创建的:
Choreographer mChoreographer;
//ViewRootImpl实例是在添加window时创建的
public ViewRootImpl(Context context, Display display) {
...
mChoreographer = Choreographer.getInstance();
...
}
看一下Choreographer.getInstance()方法:
public static Choreographer getInstance() {
return sThreadInstance.get(); //Choreographer线程单例的实现方式
}
private static final ThreadLocal<Choreographer> sThreadInstance = new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) { //当前线程必须有looper,Choreographer实例需要传入
throw new IllegalStateException("The current thread must have a looper!");
}
//为当前线程创建一个Choreographer对象
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer; //如果是UI线程赋值给成员mMainInstance
}
return choreographer;
}
};
Choreographer是线程单例的,和Looper一样是通过ThreadLocal进行存取的。
注意,当前线程一定要有looper,因为Choreographer实例需要传入。
接着看Choreographer构造方法:
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper; //当前线程的Looper
//使用当前线程looper创建mHandler
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper, vsyncSource) : null; //是否开启VSYNC,USE_VSYNC 4.1以上默认是true,表示具备接受VSync的能力。开启VSYNC后将通过FrameDisplayEventReceiver接收VSYNC脉冲信号
mLastFrameTimeNanos = Long.MIN_VALUE;
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate()); //计算一帧的时间,Android手机屏幕是60Hz的刷新频率,就是16ms
// 创建一个链表类型CallbackQueue的数组,数组大小为5,也就是数组中有五个链表,每个链表存相同类型的任务:输入、动画、遍历绘制等任务(也就是CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL)
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
Choreographer的构造方法必须传递一个Looper对象,其内部会根据该Looper创建一个FrameHandler。Choreographer的所有任务最终都会发送到该Looper所在的线程。
Choreographer的构造方法是私有的,而且是线程单例的,所以只能通过其内部的getInstance()方法获取当前线程的Choreographer实例。
③mChoreographer.postCallback安排任务
现在回头看mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)方法,该方法用于安排任务。注意到postCallback方法的第一个参数是CALLBACK_TRAVERSAL,表示回调任务的类型,共有以下5种类型:
//输入事件,首先执行
public static final int CALLBACK_INPUT = 0;
//动画,第二执行
public static final int CALLBACK_ANIMATION = 1;
//插入更新的动画,第三执行
public static final int CALLBACK_INSETS_ANIMATION = 2;
//绘制,第四执行
public static final int CALLBACK_TRAVERSAL = 3;
//提交,最后执行,
public static final int CALLBACK_COMMIT = 4;
这五种类型的任务会存入上面创建的对应的CallbackQueue中。每当收到VSync信号时,Choreographer会首先处理INPUT类型的任务,然后是ANIMATION类型,最后才是TRAVERSAL类型。
private void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
postCallback()内部调用postCallbackDelayed():
private void postCallbackDelayedInternal(int callbackType, Object 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) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();// 当前时间
// 加上延迟时间
final long dueTime = now + delayMillis;
//将任务添加到对应类型的CallbackQueue
mCallbackQueues[callbackType].addCallb ackLocked(dueTime, action, token);
if (dueTime <= now) { //立即执行
scheduleFrameLocked(now);
} else { //延迟,最终也会到scheduleFrameLocked
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
首先将任务添加到对应类型的CallbackQueue中,这里参数action就是mTraversalRunnable,token是null。CallbackQueue的addCallbackLocked()就是把dueTime、action、token组装成CallbackRecord后存入CallbackQueue的下一个节点。
如果没有延迟会执行scheduleFrameLocked()方法,有延迟就使用mHandler发送MSG_DO_SCHEDULE_CALLBACK消息,并使用msg.setAsynchronous(true)把消息设置成异步,这是因为前面设置了同步屏障,只有异步消息才会执行。
接下来看mHandler对这个消息的处理:
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,即绘制过程
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
//申请VSYNC信号,例如当前需要绘制任务时
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
//需要延迟的任务,最终还是执行上述两个事件
doScheduleCallback(msg.arg1);
break;
}
}
}
当消息为MSG_DO_SCHEDULE_CALLBACK时,会直接调用doScheduleCallback方法:
void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].ha sDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}
发现也是走到这里,即延迟运行最终也会走到scheduleFrameLocked()。看看这个方法:
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) { //开启了VSYNC
//当前执行的线程是否是mLooper所在线程
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked(); //申请VSYNC信号
} else {
// 若不在,就用mHandler发送消息到原线程,最后还是调用scheduleVsyncLocked方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);//异步
mHandler.sendMessageAtFrontOfQ ueue(msg);
}
} else { // 未开启VSYNC,则直接调用doFrame方法(4.1后默认开启)
final long nextFrameTime = Math.max( mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);//异步
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
如果系统未开启VSync机制,就直接发送MSG_DO_FRAME消息到FrameHandler。通过上面的FrameHandler代码可知,此时会直接执行doFrame方法。
Android 4.1之后系统默认开启了VSync,所以4.1之后的系统会走if分支。在Choreographer的构造方法中创建了一个FrameDisplayEventReceiver对象,这里scheduleVsyncLocked方法将会通过它申请VSYNC信号。
其中isRunningOnLooperThreadLocked方法内部根据Looper判断是否在原线程,如果在原进程就直接申请VSync信号,否则就发送MSG_DO_SCHEDULE_VSYNC消息到FrameHandler,最终还是会调用scheduleVsyncLocked方法申请VSync信号。
到这里,FrameHandler的作用就很明显了:发送异步消息(因为前面设置了同步屏障),有延迟的任务发延迟消息,不在原线程的发到原线程;没开启VSYNC的直接走doFrame方法取执行绘制。
④申请和接受VSync
接着看scheduleVsyncLocked方法是如何申请VSync信号的:
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
调用mDisplayEventReceiver的scheduleVsync()方法,mDisplayEventReceiver是Choreographer构造方法中创建的,是FrameDisplayEventReceiver的实例。 FrameDisplayEventReceiver是DisplayEventReceiver的子类,DisplayEventReceiver是一个抽象类:
public abstract DisplayEventReceiver {
public DisplayEventReceiver(Looper looper, int vsyncSource) {
if (looper == null) {
throw new IllegalArgumentException( "looper must not be null");
}
mMessageQueue = looper.getQueue();
// 注册VSYNC信号监听者
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue, vsyncSource);
mCloseGuard.open("dispose");
}
}
在DisplayEventReceiver的构造方法中通过JNI创建一个VSYNC的监听者。
FrameDisplayEventReceiver的scheduleVsync()就是在DisplayEventReceiver中:
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中断信号,会回调onVsync方法
nativeScheduleVsync(mReceiverPtr);
}
}
scheduleVsync()是通过native方法nativeScheduleVsync()去申请VSYNC信号的。
申请VSYNC信号后,当接收到VSync脉冲时会回调DisplayEventReceiver的onVsync()方法:
public abstract DisplayEventReceiver {
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
}
}
参数timestampNanos表示VSync脉冲的时间戳。
onVsync()方法的具体实现在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, long physicalDisplayId, int frame) {
long now = System.nanoTime();
if (timestampNanos > now) {
timestampNanos = now;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
//将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息
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);
}
}
在onVsync()方法中,将接收器本身作为Runnable传入异步消息msg,并使用mHandler发送msg,最终执行的就是doFrame()方法了。
注意,onVsync()方法中只是使用mHandler发送消息到MessageQueue中,不一定是立刻执行,如果MessageQueue中前面有较为耗时的操作,那么就要等它完成后才会执行本次的doFrame()。
⑤doFrame
申请VSync信号,接收到后会调用doFrame()方法,那就来看看Choreographer的doFrame():
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos; // 预期执行时间
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos; // 超时时间是否超过一帧的时间(这是因为MessageQueue虽然添加了同步屏障,但还是有正在执行的同步任务,导致doFrame延迟执行)
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos; // 计算丢帧数
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) { //丢帧超过30帧则打印Log提示
Log.i(TAG, "Skipped " + skippedFrames + " frames! The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
...
frameTimeNanos = startNanos - lastFrameOffset;
}
if(frameTimeNanos < mLastFrameTimeNanos) {
scheduleVsyncLocked();
return;
}
mFrameInfo.setVsync( intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;//Frame标志位恢复
mLastFrameTimeNanos = frameTimeNanos; //记录最后一帧时间
}
try {
// 按类型顺序执行任务
Trace.traceBegin( Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
//1
AnimationUtils.lockAnimationClock( frameTimeNanos / TimeUtils.NANOS_PER_MS);
//2
mFrameInfo.markInputHandlingStart();
doCallbacks( Choreographer.CALLBACK_INPUT, frameTimeNanos);
//3
mFrameInfo.markAnimationsStart();
doCallbacks( Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks( Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
//4
mFrameInfo.markPerformTraversalsStart();
doCallbacks( Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
//5
doCallbacks( Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
接着看任务的具体执行doCallbacks方法:
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
final long now = System.nanoTime();
// 根据指定的类型在CallbackQueue中查找到达执行时间的CallbackRecord
callbacks = mCallbackQueues[callbackTy pe].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
}
try {
for (CallbackRecord c = callbacks; c != null; c = c.next) { // 迭代执行队列中的所有任务
c.run(frameTimeNanos); //回调CallbackRecord的run,其内部回调Callback的run
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
//回收CallbackRecord
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
}
}
主要就是取对应任务类型的队列,遍历队列执行所有任务,执行任务是CallbackRecord的run方法:
private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; //Runnable or FrameCallback
public Object token;
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) { //通过postFrameCallback或postFrameCallbackDelayed会执行这里
((FrameCallback)action).doFrame( frameTimeNanos);
} else {
((Runnable)action).run(); //取出Runnable执行run()
}
}
}
前面mChoreographer.postCallback传的token是null,所以取出action,就是Runnable,执行run(),这里的action就是ViewRootImpl发起的绘制任务mTraversalRunnable了,那么这样整个逻辑就闭环了。
那啥时候 token == FRAME_CALLBACK_TOKEN 呢?答案是Choreographer的postFrameCallback()方法:
public void postFrameCallback(FrameCallback callback) {
postFrameCallbackDelayed(callback, 0);
}
public void postFrameCallbackDelayed( FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException( "callback must not be null");
}
//也会postCallbackDelayedInternal,并且注意是CALLBACK_ANIMATION类型,token是FRAME_CALLBACK_TOKEN,action就是FrameCallback
postCallbackDelayedInternal( CALLBACK_ANIMATION, callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
public interface FrameCallback {
public void doFrame(long frameTimeNanos);
}
可以看到postFrameCallback()传入的是FrameCallback实例,接口FrameCallback只有一个doFrame()方法。并且也是走到postCallbackDelayedInternal,FrameCallback实例作为action传入,token则是FRAME_CALLBACK_TOKEN,并且任务是CALLBACK_ANIMATION类型。
Choreographer的postFrameCallback()通常用来计算丢帧情况,使用方式如下:
Application.java:
public void onCreate() {
super.onCreate();
//在Application中使用postFrameCallback
Choreographer.getInstance().postFrameCa llback(new FPSFrameCallback( System.nanoTime()));
}
public class FPSFrameCallback implements Choreographer.FrameCallback {
private static final String TAG = "FPS_TEST";
private long mLastFrameTimeNanos = 0;
private long mFrameIntervalNanos;
public FPSFrameCallback(long lastFrameTimeNanos) {
mLastFrameTimeNanos = lastFrameTimeNanos;
mFrameIntervalNanos = (long)(1000000000 / 60.0);
}
@Override
public void doFrame(long frameTimeNanos) {
//初始化时间
if (mLastFrameTimeNanos == 0) {
mLastFrameTimeNanos = frameTimeNanos;
}
final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if(skippedFrames>30){
//丢帧30以上打印日志
Log.i(TAG, "Skipped " + skippedFrames + " frames! " + "The application may be doing too much work on its main thread.");
}
}
mLastFrameTimeNanos = frameTimeNanos;
//注册下一帧回调
Choreographer.getInstance().postFram eCall back(this);
}
}
所以,使用Choreographer的postCallback()、postFrameCallback()的作用:发送任务,存到队列中,监听VSync信号,当VSync到来时会使用mHandler发送异步message,这个message的Runnable就是队列中的所有任务。
面试问答:
①丢帧/掉帧是说这一帧延迟显示还是丢弃不再显示 ?
答:延迟显示,因为缓存交换的时机只能等下一个VSync了。
②布局层级较多/主线程耗时,是如何造成丢帧的呢?
答:布局层级较多/主线程耗时,会影响CPU/GPU的执行时间,大于16.6ms时只能等下一个VSync了。
③16.6ms刷新一次是啥意思?是每16.6ms都走一次measure/layout/draw ?
答:屏幕的固定刷新频率是60Hz,即16.6ms。不是每16.6ms都走一次measure/layout/draw,而是有绘制任务才会走,并且绘制时间间隔是取决于布局复杂度及主线程耗时。
④measure/layout/draw走完,界面就立刻刷新了吗?
答:不是。measure/layout/draw走完后,会在VSync到来时进行缓存交换和刷新。
⑤如果界面没动静止了,还会刷新吗?
答:屏幕会固定每16.6ms刷新,但CPU/GPU不走绘制流程。
⑥可能你知道VSYNC,这个具体指啥?在屏幕刷新中如何工作的?
答:当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时会出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。并且Android4.1后 CPU/GPU的绘制是在VSYNC到来时开始。
⑦可能你还听过屏幕刷新使用双缓存、三缓存,这又是啥意思呢?
答:双缓存是Back buffer、Frame buffer,用于解决画面撕裂。三缓存增加一个Back buffer,用于减少Jank。
⑧可能你还听过神秘的Choreographer,这又是干啥的?
答:用于实现——"CPU/GPU的绘制是在VSYNC到来时开始"。