一:人眼刷新频率
- 12fps:由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒10-12帧,认为是连贯。
- 24fps:有声电影拍摄及播放帧率为24hz。
- 30fps:电子游戏,帧率少于30fps,肉眼会觉得不连贯。
- 60fps:在与手机交互中,如触摸和反馈60帧以下是可以感觉出来(卡顿),但如果大于60fps不能察觉变化(之快)。
HZ:是频率的单位,频率是指电脉冲、交流电波形、电磁波等1秒钟重复的次数。
fps:是图像领域中的定义,是指画面每秒传输帧数。
因此Android系统每隔16ms发出VSYNC信号,触发对UI的渲染,如果每次渲染都成功,这样就能够达到流畅画面所需的60fps,为了能够实现60fps,因此每次绘制都要在16ms内完成。
二:刷新屏幕机制
- 应用程序向系统服务申请一块buffer(缓存),系统服务返回buffer,应用拿到buffer后就可以进行绘制。
- 应用在buffer上绘制好了之后,将buffer提交给系统服务。
- 系统服务将buffer写到屏幕的一块缓存区,屏幕会以一定的帧率刷新,每次刷新时,就会从缓存区将图像数据读取显示出来。
- 如果缓存区没有新的数据,就一直用旧的数据,这样屏幕看起来就没有变。
在这个过程中:
- CPU:负责计算帧数据,measure、layout,负责把UI组件计算成Texture纹理,然后交给GPU进行栅格化渲染。
- GPU:对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)中存起来,栅格化数据。栅格化是绘制那些button、shape、bitmap等组件最基础的操作,他把那些组件拆分到不同的像素上显示,这是一个很费事的操作,GPU的引入就是为了加快栅格化(Rasterization)的操作。
- Display:屏幕或者显示器,负责把buffer中的数据呈现到屏幕上
buffer个数问题:
- 单buffer:当buffer数据正在被屏幕读取显示时,下一帧数据写入buffer,将会导致屏幕显示多帧内容。
- 2个buffer:显示屏上的内容,是从硬件帧缓冲区读取的。从buffer的起始地址开始,从上往下从左至右扫描整个buffer,将内容映射到显示屏上。一个FrontBuffer用于提供屏幕显示内容,backBuffer用于后台合成下一帧图形。当frontBuffer内容显示完毕,交换Front和Back角色,front作为下一帧写入缓冲,back则给屏幕提供数据。
三:VSync垂直同步机制
页面刷新分为:Display刷新(从buffer中读取数据) + CPU/GPU合成帧数据(帧数据写入buffer)。
如果display刷新频率和GPU/CPU合成帧率不同步,则会出现以下问题:
在该图中,由于第2帧CPU开始处理帧数据晚了,导致GPU处理第2帧也晚了,导致第1帧数据展示了2次(出现掉帧)。如果第二帧CPU处理时间能提前到红色框的时间点,也许不会出现掉帧现象。这就是Display刷新频率和CPU/GPU合成频率不一致导致的。
引入Vsync信号同步后:
可以看到所有的帧都在vsync信号到来后,CPU就开始处理帧数据,也就解决了刷新不同步的问题。
四:choreography
之前,屏幕的vsync信号只是用来控制帧缓冲区的切换,并未控制上层的绘制节奏,也就是我们刚刚说的display刷新频率和CPU/GPU合成频率是脱离的。
因此google加入了上层接受垂直同步信号的逻辑:
主要就是通过Choreographer来实现的,流程如下:
五:DisplayList
Android需要把XML布局文件转换成GPU能够识别并绘制的对象,这个操作是在DisplayList的帮助下完成的。Display List是一个缓存绘制命令的buffer,他本质是一个缓冲区,里面记录了将要执行的绘制命令序列。
Display List是视图的基本绘制元素,包含元素原始属性(位置、尺寸、透明度等),对应Canvas的drawXXX方法。
视图信息传递流程:Canvas(Java API)-> OpenGL(C/C++ Lib) -> 驱动程序 ->GPU
在硬件加速渲染环境中,Android应用程序窗口的UI渲染是分两步进行的:
- 第一步是构建DisplayList,CPU在measure、layout、draw之后,生成了DisplayList,是在主线程中运行的。
- 第二步是渲染Display List,是在应用程序进程的RenderThread中进行的。增加Render Thread线程,也是为了避免UI线程任务过重,用于提高渲染性能。
DisplayList命令最终会转化为Open GL命令,由GPU执行,这意味着我们在调用CanvasAPI绘制UI时,实际上只是将Canvas API调用及其参数记录在DisplayList中。
五:surface、surfaceFlinger
每一个Activity组件都关联一个或若干个窗口,每一个窗口都对应一个surface。有了这个surface之后,应用程序就可以在上面渲染窗口的UI。
最终这些已经绘制好的surface都会被统一提交给surface管理服务SurfaceFlinger进行合成,最后显示在屏幕上面。
- surface:android应用的每个窗口对应一个画布(canvas),即surface。surface是一个接口,供生产方与使用方交换缓冲区。
- surface flinger:android系统服务,在开机时初始化该服务,该服务也会注册到ServiceManager中,负责android系统的帧缓冲区,即显示屏幕。
surfaceFlinger用来管理消费当前可见的Surface,所有被渲染的可见Surface都会被Surface Flinger通过WindowManager提供的信息合成(使用Open GL和Hardware Composer)提交到屏幕的后缓冲区,等待屏幕的下一个VSync到来,再显示到屏幕上。
六:具体代码流程
设置同步屏障可以保证在vsync回来时,可以立马开始在主线程进行计算绘制数据,避免超时。
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
public void scheduleVsync() {
nativeScheduleVsync(mReceiverPtr); //调用native请求vsync回调
}
// Called from native code.
private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame) { //该方法被native回调
onVsync(timestampNanos, physicalDisplayId, frame);
}
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
//...忽略部分code,发送一个消息,去执行this这个runnable,也就是会触发到该类的run方法
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函数,会执行第二段代码里的mTraversalRunnable代码。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
doTraversal这个方法里会去执行performTraversal。所以measure、layout、draw是在onVsync回调后才执行。
\
应用程序通过调用viewRootImpl.requestLayout发起重绘,通过choreography发送异步消息,请求同步vsync信号。即在下一次vsync信号过来时,系统服务surfaceFlinger在第一时间通知我们,触发UI绘制。虽然可以多次调用requestLayout,但是在一个vsync周期内,requestLayout只会执行一次。
七:常见问题
1.丢帧一般是什么原因引起的?
答:主线程有耗时操作,耽误了view的绘制
2.Android刷新频率60帧/秒,每隔16ms调onDraw绘制一次?
答: 60帧/秒也是vsync信号的频率,但不一定每次vsync信号都会去绘制,先要应用端主动发起重绘,才会向SurfaceFlinger请求接收vsync信号,这样当vsync信号来的时候,才会真正去绘制。
3.onDraw执行完之后屏幕会马上刷新么?
答: 不会马上刷新,会等到下一次vsync信号时才会刷新。
4.如果界面没有重绘,还会每隔16ms刷新屏幕么?
答:界面没有重绘,应用就不会收的vsync信号,屏幕还是会刷新,画面数据用的是旧的,看起来没什么变化而已
5.如果屏幕快要刷新的时候才去onDraw绘制会丢帧么?
答: 重绘不会立即执行,而是等到下一次vsync信号来时才开始, 所以什么时候发起重绘影响不大