使用Vsync信号的由来
显示屏上的内容,是从硬件帧缓冲中去读取的,大致读取过程为:从Buffer的起始地址开始,从上往下,从左往右扫描整个Buffer,将内容映射到显示屏上。而这个图像内容的来源就源于App给的数据了。
如果对缓冲区同时进行读写,可能会出现屏幕画面撕裂的情况。 如何解决呢?可以采用前缓冲区和后缓冲区两个区域协同。
在某个时刻,两个缓冲区进行数据交换,就可以完成所有图像的绘制了。
屏幕刷新率(HZ):代表屏幕在一秒内刷新屏幕的次数,Android手机一般为60HZ(也就是1秒刷新60帧,大约16.67毫秒刷新1帧) 系统帧速率(FPS):代表了系统在一秒内合成的帧数,该值的大小由系统算法和硬件决定。
实际运行过程中,可能存在以下情况:
1、屏幕刷新速率比系统帧速率快此时,在前缓冲中区内容全部映射到屏幕上之后,后缓冲中区尚未准备好下一帧,屏幕将无法读取下一帧,所以只能继续显示当前一帧的图形,造成一帧显示多次,也就是卡顿。
2、系统帧速率比屏幕刷新率大 此时,屏幕未完全把前缓冲区的一帧映射到屏幕,而系统已经在后缓冲区准备好了下一帧,并要求读取下一帧到屏幕,将会导致屏幕上半部分是上一帧的图形,而下半部分是下一帧的图形,造成屏幕上显示多帧,也就是屏幕撕裂。上面两种情况,都会导致问题,根本原因就是两个缓冲区的操作速率不一致,解决办法就是让屏幕控制前后缓冲中区的切换,让系统帧速率配会屏幕刷新率的节奏。
出现这个问题的原因是系统的刷新率与屏幕的刷新率达到一致,没有一个统一的方式来控制生成与显示,本质就是生产者与消费者的供需关系无法保证达到平衡。。因此,我们需要某种方式来解决这种问题,这就是Vsync信号。在一个Vsync信号中,我们处理一次生成与显示。这样就能保证系统的刷新率与屏幕的刷新率达到一致。
Android上的Vsync
Display就代表屏幕,CPU代表图片的生成,我们使用的图大多是位图,有很多的像素点。每一个点有自己的颜色值。将所有的点描述出来就可以显示出来最终的效果了。每一个像素点由RBG来控制。GPU的作用就是将色值转化成RGB值,另外还有图片的缩放,也就是栅格化。
这个图里边还有个问题,就是我们会不断的申请新的空间,有没有办法做到像Handler那样,使用回收复用的机制? 这个能力其实被实现了,使用DoubleBuffer去实现了。减少了性能浪费。
上图还有个问题,就是如果图像的合成超出了一帧的时间,就会造成卡顿。
这里边有一些时间没有被利用起来。有没有办法继续利用起来呢? Android中还真的实现了,使用了3缓冲。
三缓冲对于Jank的影响
以60HZ屏幕为例,jankyframes是绘制时间大于16ms的帧,但由于3缓冲机制,其实很可能不会发生真正的jank(视觉停留在屏幕上的时间多于16ms)。在弹力球测试时发现,即使发生20次绘制janky frames,仍然不会发生一次真正的iank(高通平台可通过mdssfb*0查看真正的jank)。这就是三缓冲的作用。
SF完成图像的合成
先看一个例子:
我们可以先这样理解上面这幅图,上层每一个界面,其实都对应sufaceflinger里的一个Surface对象,上层将自己的内容绘制在对应的Surface内,接着,SufaceFlinger需要将所有上Surface内的图形进行会成,
一个window对应一个Surface, 一个Surface对应一个Bufferqueue,Surface不能只能拿到BufferQueue,需要通过这个生产者来获取。
Surface内部提供一个BufferQueue,与上层和Surfaceflinger形成一个生产者消费者模型,上层对应Producer,SurfaceFlinger对应229621398Consumer。三者通过Buffer产生联系,每个Buffer都有四种状态:
- Free:可被上层使用
- Dequeued:出列,正在被上层使用
- Queued:入列,已完成上层绘制,等待SurfaceFlinger合成
- Acquired:被获取,SurfaceFlinger正持有该Buffer进行合成 硬件合成与软件合成的区别:软件合成会合成完整的图片,就是全部屏幕的数据,硬件合成只是记录每个图元的位置。然后给到屏幕去做最终的显示。
Buffer的生命周期:free -> dequeued->queued ->acquired-> free
BufferSlot的大小是64。
这个Unused Slot表示还没有使用的,Use slot表示使用过的。用过的里边又分为unactive和active。 生产者相当于Surface在SF的一个代理,这个生产者从BufferQueue中取到需要的Buffer,也就是FreeSlot里边,然后交给Surface去使用。但是Surface其实也并不是直接使用,最终使用的是我们的View。Surface将数据赋值之后,会把这些数据交给生产者,然后生产者再交给BufferQueue,消费者端也是类似。硬件需要合成的时候也是通过消费者去获取数据的。 Java这边取数据,这个canvas就是缓冲数据。
整体的图像合成架构就是这样的
在View的draw方法的时候进行出队,完成数据的写入。之后完成入队。
为什么一定要弄一个生产者和消费者呢?因为应用不能直接去读取系统的内容,这是出于安全的考虑。Surface作为应用端的生产者,同时也做为了屏幕的消费者。通过Surface完成了屏幕与app的解耦。
正常情况下,Queue里边应该只有一个Buffer,如果有多个的话,说明出现卡顿了。比如下图:
SurfaceView
什么是SurfaceView?一个拥有单独画布的View,也就是有一个Canvas,相当于在Window上挖了一个洞,给SurfaceView做显示。
SurfaceView的使用
public class GameUI extends SurfaceView implements SurfaceHolder.Callback {
private SurfaceHolder holder;
private RenderThread renderThread;
private boolean isDraw = false;
public GameUI(Context context) {
super(context);
}
public GameUI(Context context, AttributeSet attrs) {
super(context, attrs);
holder = this.getHolder();
holder.addCallback(this);
renderThread = new RenderThread();
}
public GameUI(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
}
/**
* 绘制界面的线程
*/
private class RenderThread extends Thread {
@Override
public void run() {
while (isDraw) {
drawUI();
}
super.run();
}
}
private void drawUI() {
Canvas canvas = holder.lockCanvas(); // 相当于dequeue一个出来,用来写数据
try {
drawCanvas();
}catch (Exception e) {
} finally {
holder.unlockCanvasAndPost(canvas);// 相当于queue一个出来,用来将数据放回到队列中。
}
}
private void drawCanvas() {
}
}
View和SufaceView的区别,view必须要放到Window中,而SurfaceView可以在任何线程中。同时SurfaceView可以控制帧数。
requestLayout如何向SurfaceFlinger申请Surface?