Android进阶宝典 -- Android系统图像绘制原理大全解

1,516 阅读11分钟

在上一篇文章 Android进阶宝典 -- WindowManager原理深度分析,着重介绍了WindowManager对于窗口的控制,例如什么时候添加view,以及窗口的刷新机制。通过上节我们简单介绍了UI的刷新流程,以及图像渲染进程SurfaceFlinger,那么这节我将会着重介绍Android系统的图像显示及刷新原理。

1 Android系统的图像显示

当我们打开某个app时,多彩的画面就会展示在手机屏幕上,只要有数据变动画面就会改变,那么这些画面是如何展示在手机上的,画面数据是存储在哪里的?

有关Android图形的官方文档:

source.android.com/docs/core/g…

1.1 硬件帧缓冲区

这里是一个新的概念,Android设备的屏幕可以抽象为一个(硬件)帧缓冲区,在系统的HAL层是存在一个Gralloc模块,内部封装了所有对帧缓冲区的访问操作。 当然一个Android设备至少会有一个显示屏用于展示图形,当这个画面需要刷新时,会往帧缓冲区写入要绘制的画面内容,既然有写那么就会有读,当帧缓冲区的数据准备好后,就会将帧缓冲区中的数据读取出来,渲染到屏幕上。

当然这也只是一个比较简单的介绍,有兴趣的伙伴可以去看下HAL层的源码,其中会介绍Gralloc模块,以及gralloc设备与fb设备。

image.png

那么既然涉及到了读写操作,必然会有同步的问题发生;如果需要绘制的画面数据没有准备好,那么就去读数据,就会造成图像帧不同步,屏幕画面出现撕裂。

其实在多线程中,解决同步的一种处理方式就是加锁,在读写操作持有同一把锁,当写操作的时候,读操作就需要等待,只有等到写操作完成之后释放锁,交由读操作执行。

但是加锁带来的一个问题就是性能稍差一点,同一时间只有读或者写,效率比较低,除此之外还有什么方法呢?就是增加缓冲区。

1.2 多缓冲区解决屏幕撕裂问题

采用多缓冲区的目的就是为了提高效率,让读写可以同时进行。前缓冲区用于屏幕直接数据读取,后缓冲区则是用于写入数据。

image.png

所以类似于另开了一个线程,每个线程只会干自己的事情,但是这里并不是说后缓冲区的数据写完之后,还会传输到前缓冲区,而是会在一个时机进行交换,前缓冲区变后缓冲区,后缓冲区变前缓冲区。

1.3 从帧缓冲区了解卡顿的原因

前面我们介绍的缓冲区图形读写流程是理想的,当前缓冲区的数据读取完成之后,后缓冲区的数据刚好写完,但是实际上这种场景很难出现,因为一旦两者的速率不一致,就可能造成卡顿或者屏幕撕裂。

通常我们在买手机的时候,会看下设备参数:

分辨率:2400 x 1080
显示帧率:最高 120Hz

这里的显示帧率指的就是每秒屏幕的刷新速率,最高120HZ,也就是说最高每秒120次;还有一个参数叫FPS,这个参数代表系统每秒合成的帧数,通常由硬件性能决定的,如果玩过LOL,屏幕右上角会显示FPS帧数,如果低于60FPS,就会感觉到卡顿。

  • 刷新帧率HZ 大于 合成帧率FPS

通过对这两个参数的比较,如果显示帧率要大于合成帧率,也就意味着前缓冲区的数据刷新完成之后,后缓冲区的数据帧还没有准备好,此时屏幕依然显示上一帧,就造成了卡顿。

  • 合成帧率HZ 大于 刷新帧率HZ

这里刚好跟上面的反着,当后缓冲区的数据写完之后,前缓冲区的数据还没渲染完,就请求渲染下一帧的数据了,导致出现屏幕撕裂,例如下图:上半部分展示的还是上一帧图像,下半部分就展示下一帧的图像了。

image.png

所以为了保证刷新速率与合成速率的一致,才有了垂直同步信号的概念,通过同步信号约束图像合成与刷新,使其步调一致。

image.png

也就是说当我的屏幕需要刷新的时候,才会去合成图像写入后缓冲区,然后屏幕去前缓冲区获取数据刷新。

2 VSYNC同步信号与三缓冲

这里官方对于VSYNC信号的解释,其主要作用就是为了同步渲染展示的流程,防止卡顿。

VSYNC 信号可同步显示流水线。显示流水线由应用渲染、SurfaceFlinger 合成以及用于在屏幕上显示图像的硬件混合渲染器 (HWC) 组成。VSYNC 可同步应用唤醒以开始渲染的时间、SurfaceFlinger 唤醒以合成屏幕的时间以及屏幕刷新周期。这种同步可以消除卡顿,并提升图形的视觉表现

看下面这张图:

image.png

  • 我们先看第一帧的生成,通过CPU的计算以及GPU的栅格化处理,合成了第一帧,当VSYNC信号来了之后,第一帧被Display展示在了屏幕上,此时刷新速率与合成速率是一致的
  • 当开始合成第二帧的时候,我们发现当VSYNC信号来的时候,图像并没有合成完成,因此展示的还是第一帧,因为后缓冲区数据没写完,前缓冲区数据为上一帧的数据。
  • 当第三个VSYNC信号来的时候,缓存实现了交换,此时正常展示了第二帧的数据。

2.1 Android 4.1 后对于VSYNC的优化

本小节开头介绍了Android 4.1 之前的刷新机制,只有在数据帧合成完成,并完成缓存交换之后,才会进行下一帧的合成,那么势必就会浪费掉一些时间,看下图:

image.png

中间的时间明明可以进行下一帧的渲染,从而避免一次Jank,因此在Android 4.1之后进行了优化,见下图:

image.png

只有当接收到Vsync信号之后,才开始进行图像合成,此时CPU和GPU将会有完整的16ms来处理,当下次VSYNC信号来临时,进行缓存交换展示。

2.2 三缓冲机制

但是即便如此,如果因为页面复杂,例如层级嵌套深,会导致CPU和GPU的处理时间增长,从而导致在下次VSYNC信令来临时,图像没有合成,导致无法交换缓存空间,从而产生了卡顿,例如下图:

image.png

因为现在是双缓存机制,会导致第一次VSYNC和第二次VSYNC之间很长一段CPU GPU时间被浪费了,因此为了避免卡顿,引入了三缓冲的机制,其实就是新申请一块Graphic Buffer内存,如下图:

image.png

当VSYNC信号来临时,因为没有合成图像,所以没有进行缓存交换,但是为了避免CPU浪费,开启了C帧的合成,此时B帧也合成完成,下次VSYNC信号来临时,成功渲染B帧,以此类推从而减少了一次卡顿。

所以VSYNC虽然保持了刷新频率与合成频率的步调一致,但是依然不能完全避免卡顿的发生;因为内部合成速度的问题,如果因为耗时计算导致了CPU和GPU需要花更多的时间去合成一帧图像,就会导致缓冲区内的数据不能及时更新从而产生卡顿 ,所以系统从底层做了优化,采用了三缓冲机制。

所以有伙伴可能会想,多加几个buffer岂不是更好的避免了卡顿,当然这样做是能解决问题,但是内存会陡然上升,影响性能。

3 图像合成 SurfaceFlinger

前面我们介绍了当VSYNC信号来临时,CPU和GPU开始工作合成图像,那么具体合成图像的进程就是SurfaceFlinger。

SurfaceFlinger 接受缓冲区,对它们进行合成,然后发送到屏幕。WindowManager 为 SurfaceFlinger 提供缓冲区和窗口元数据,而 SurfaceFlinger 可使用这些信息将 Surface 合成到屏幕。

3.1 Window与Surface的关系

在上节中,我们介绍了Window和Activity的关系,因为Window是承载画面的窗体,所以Window跟Surface也是一一对应的,Surface对象使应用能够渲染要在屏幕上显示的图像,像OpenGL就是将画面渲染在Surface上,而SurfaceFlinger则是将Surface合成到屏幕上,用户在手机屏幕上就可以看到这些画面。

所以Surface与SurfaceFlinger是存在直接的关系的,他们之间的关系是一种常见的模型:生产者与消费者,Surface用于生产数据帧,而SurfaceFlinger则是用于消费数据帧,以便合成数据帧。

image.png

当我们通过WindowManager添加一个View之后,ViewRootImpl中会执行scheduleTraversals方法进行测量绘制,当进行绘制的时候,就会使用到SurfaceFlinger。

  • 首先在SurfaceFlinger中存在一个BufferQueue,用于数据帧的入列和出列,其中会有一个生产者和消费者模型,生产者就是Surface,在应用侧拿到的Surface其实就是在SurfaceFlinger中消费者的代理对象;
  • 当进行绘制时,Surface就会渲染所要展示的图像,例如Text、Button等,绘制完成之后就会生成对应的数据帧交给BufferQueue;
  • 当VSYNC信号来临时,HWC会从消费者中取出要展示在屏幕上的数据帧展示,同时完成缓存Buffer替换。

3.2 SurfaceView

官方文档:

source.android.com/docs/core/g…

提到SurfaceView,可能很多伙伴会在一些面试题中看到,但是实际用到却很少。这也是正常的,是因为原生Skia渲染器通常是在Canvas上渲染页面,这里不需要开发者自己去实现渲染逻辑;但是如果碰到使用OpenGL去渲染画面,例如摄像头的预览画面,在一些直播场景中会使用到,往往需要自采集数据并渲染,这时就会使用到SurfaceView。

SurfaceView看名字就知道也是一个控件,与其他控件不同的是,它内部有一个Surface,我们不能直接拿到Surface,因此Android提供了SurfaceHolder用来对Surface进行封装。

class CameraPreView @JvmOverloads constructor(
    context: Context
) : SurfaceView(context), SurfaceHolder.Callback {
    /**是否在绘制*/
    private var isDrawing = false
    private var renderHandler:RenderHandler

    init {
        renderHandler = RenderHandler()
    }


    override fun surfaceCreated(holder: SurfaceHolder) {
        isDrawing = true
        //开启
        renderHandler.start()
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {

    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        isDrawing = false
    }

    inner class RenderHandler : Thread(){
        override fun run() {
            super.run()
            while(isDrawing){
                val canvas = holder.lockCanvas() // 申请缓冲区
                //开启绘制
                try {
                    drawCanvas(canvas)
                }catch (e:Exception){
                
                }finally {
                    holder.unlockCanvasAndPost(canvas)
                }
            }
        }

        private fun drawCanvas(canvas: Canvas?) {
            canvas?.drawText("长安三万里")
        }
    }

}

首先我们看下上面的自定义View,这里其实就是在做自定义的绘制,但是跟普通的View不同的是,这里的绘制是可以放在子线程的。

这就是SurfaceView与普通的View不同之处,Android要求在使用SurfaceView时需要使用主线程之外的线程渲染Surface,因此一些耗时的渲染任务就可以放在子线程中进行,防止阻塞主线程。

如果想要监听Surface的创建或者销毁,需要实现SurfaceHolder的Callback接口,当Surface创建成功之后,就会回调surfaceCreated接口,此时可以开启渲染线程;当Surface销毁之后,会回调surfaceDestroyed接口,停止渲染。

当开启Surface渲染之后,可以获取SurfaceHolder对象,这是SurfaceView的内部对象,前面我们提到过SurfaceHolder是对Surface的一系列封装,我们介绍其中比较核心的API接口。

  • lockCanvas

当调用lockCanvas时,会向SurfaceFlinger申请缓冲区以写入pixel,并且会锁定这个缓冲区不会被其他客户端同时写入,同时会返回Canvas对象;

当返回Canvas对象之后,可以通过画笔来绘制图形,类似于执行onDraw的流程。

  • unlockCanvasAndPost

当往缓冲区写完数据帧之后,调用unlockCanvasAndPost会解锁缓冲区,并将其发送到合成器也就是SurfaceFlinger,此时生产者完成了一次绘制的任务入队BufferQueue,当屏幕需要刷新时,就会从BufferQueue中取出数据,通过HWC合成到屏幕中。

3.3 View和SurfaceView的区别

这里我们总结一下普通的View和SurfaceView的区别:

  • 对于普通的View,其实是处于Window之上的,可以通过WindowManager添加;而SurfaceView内部是有一个Surface,绘制的内容是渲染在Surface上的;
  • 对于自定义View,是需要放在主线程刷新的,这是Android系统的规则;而SurfaceView对于UI的刷新是可以放在子线程的,这也是Android系统的规则,可以提高性能。
  • 对于普通View来说,其刷新机制依赖于底层的规则,60HZ是Android系统的标准;而SurfaceView因为可以独立线程渲染,人为可以控制,因此可以设置刷新的速率。