Android图形显示系统浅析

2 阅读6分钟

使用Vsync信号的由来

显示屏上的内容,是从硬件帧缓冲中去读取的,大致读取过程为:从Buffer的起始地址开始,从上往下,从左往右扫描整个Buffer,将内容映射到显示屏上。而这个图像内容的来源就源于App给的数据了。

image.png

如果对缓冲区同时进行读写,可能会出现屏幕画面撕裂的情况。 如何解决呢?可以采用前缓冲区和后缓冲区两个区域协同。

image.png

在某个时刻,两个缓冲区进行数据交换,就可以完成所有图像的绘制了。

image.png

屏幕刷新率(HZ):代表屏幕在一秒内刷新屏幕的次数,Android手机一般为60HZ(也就是1秒刷新60帧,大约16.67毫秒刷新1帧) 系统帧速率(FPS):代表了系统在一秒内合成的帧数,该值的大小由系统算法和硬件决定。

实际运行过程中,可能存在以下情况:

1、屏幕刷新速率比系统帧速率快此时,在前缓冲中区内容全部映射到屏幕上之后,后缓冲中区尚未准备好下一帧,屏幕将无法读取下一帧,所以只能继续显示当前一帧的图形,造成一帧显示多次,也就是卡顿。

2、系统帧速率比屏幕刷新率大 此时,屏幕未完全把前缓冲区的一帧映射到屏幕,而系统已经在后缓冲区准备好了下一帧,并要求读取下一帧到屏幕,将会导致屏幕上半部分是上一帧的图形,而下半部分是下一帧的图形,造成屏幕上显示多帧,也就是屏幕撕裂。上面两种情况,都会导致问题,根本原因就是两个缓冲区的操作速率不一致,解决办法就是让屏幕控制前后缓冲中区的切换,让系统帧速率配会屏幕刷新率的节奏。

出现这个问题的原因是系统的刷新率与屏幕的刷新率达到一致,没有一个统一的方式来控制生成与显示,本质就是生产者与消费者的供需关系无法保证达到平衡。。因此,我们需要某种方式来解决这种问题,这就是Vsync信号。在一个Vsync信号中,我们处理一次生成与显示。这样就能保证系统的刷新率与屏幕的刷新率达到一致。

image.png

image.png

image.png

Android上的Vsync

image.png

Display就代表屏幕,CPU代表图片的生成,我们使用的图大多是位图,有很多的像素点。每一个点有自己的颜色值。将所有的点描述出来就可以显示出来最终的效果了。每一个像素点由RBG来控制。GPU的作用就是将色值转化成RGB值,另外还有图片的缩放,也就是栅格化。

这个图里边还有个问题,就是我们会不断的申请新的空间,有没有办法做到像Handler那样,使用回收复用的机制? 这个能力其实被实现了,使用DoubleBuffer去实现了。减少了性能浪费。

image.png

上图还有个问题,就是如果图像的合成超出了一帧的时间,就会造成卡顿。

image.png

这里边有一些时间没有被利用起来。有没有办法继续利用起来呢? Android中还真的实现了,使用了3缓冲。

image.png 三缓冲对于Jank的影响

以60HZ屏幕为例,jankyframes是绘制时间大于16ms的帧,但由于3缓冲机制,其实很可能不会发生真正的jank(视觉停留在屏幕上的时间多于16ms)。在弹力球测试时发现,即使发生20次绘制janky frames,仍然不会发生一次真正的iank(高通平台可通过mdssfb*0查看真正的jank)。这就是三缓冲的作用。

SF完成图像的合成

先看一个例子:

image.png 我们可以先这样理解上面这幅图,上层每一个界面,其实都对应sufaceflinger里的一个Surface对象,上层将自己的内容绘制在对应的Surface内,接着,SufaceFlinger需要将所有上Surface内的图形进行会成,

一个window对应一个Surface, 一个Surface对应一个Bufferqueue,Surface不能只能拿到BufferQueue,需要通过这个生产者来获取。

image.png

image.png Surface内部提供一个BufferQueue,与上层和Surfaceflinger形成一个生产者消费者模型,上层对应Producer,SurfaceFlinger对应229621398Consumer。三者通过Buffer产生联系,每个Buffer都有四种状态:

  • Free:可被上层使用
  • Dequeued:出列,正在被上层使用
  • Queued:入列,已完成上层绘制,等待SurfaceFlinger合成
  • Acquired:被获取,SurfaceFlinger正持有该Buffer进行合成 硬件合成与软件合成的区别:软件合成会合成完整的图片,就是全部屏幕的数据,硬件合成只是记录每个图元的位置。然后给到屏幕去做最终的显示。

Buffer的生命周期:free -> dequeued->queued ->acquired-> free

image.png

BufferSlot的大小是64。

image.png

这个Unused Slot表示还没有使用的,Use slot表示使用过的。用过的里边又分为unactive和active。 生产者相当于Surface在SF的一个代理,这个生产者从BufferQueue中取到需要的Buffer,也就是FreeSlot里边,然后交给Surface去使用。但是Surface其实也并不是直接使用,最终使用的是我们的View。Surface将数据赋值之后,会把这些数据交给生产者,然后生产者再交给BufferQueue,消费者端也是类似。硬件需要合成的时候也是通过消费者去获取数据的。 Java这边取数据,这个canvas就是缓冲数据。 image.png

整体的图像合成架构就是这样的

image.png

在View的draw方法的时候进行出队,完成数据的写入。之后完成入队。

为什么一定要弄一个生产者和消费者呢?因为应用不能直接去读取系统的内容,这是出于安全的考虑。Surface作为应用端的生产者,同时也做为了屏幕的消费者。通过Surface完成了屏幕与app的解耦。

image.png

正常情况下,Queue里边应该只有一个Buffer,如果有多个的话,说明出现卡顿了。比如下图:

image.png

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?

image.png