Surafce 图形系统总结

327 阅读9分钟

img

Buffer1 : 图形缓冲区

Buffer2: 硬件帧缓冲区

1. 图形渲染流程

1.1 app层绘制

由ViewRootImpl 发起 performTraversals 开始 View 的绘制

  1. 测量View的宽高(Measure)
  2. 设置View的宽高位置(Layout)
  3. 创建显示列表,并执行绘制(Draw)
  4. 绘制通过图形处理引擎来实现,生成多变形和纹理(Record、Exceute)

其中引擎包括:

**2D: ** Canvas , Canvas 调用的 API 到下层其实封装了 skia 的实现

**3D:**OpenGL ES , 当应用窗口flag = WindowManager.LayoutParams.MEMORY_TYPE_GPU,则表示需要用OpenGl接口来绘制UI.

1.2 Surface

每个window 对应一个surface ,任何view 都要画在surface 的canvas 上。

图形的传递是通过Buffer作为载体,surface是对Buffer的进一步封装

surface内部具有多个Buffer供上层使用。

img

surface对生产者代理对象,surface (native)对应生产者本地对象。

流程:

  1. 上层app通过surface获取buffer,供上层绘制,绘制过程通过canvas来完成,底层实现是skia引擎。

  2. 绘制完成后数据通过surface被queue进BufferQueue

  3. 然后监听会通知surfaceflinger去消费buffer

  4. 接着surfaceFlinger就acquie数据拿去合成

  5. 合成完成后将 buffer release 回bufferqueue

  6. 如此循环,形成一个buffer被循环使用的过程

状态

  1. free : 可被上层使用
  2. Dequeued : 出列,正在被上层使用
  3. Queued: 入列,已被上层绘制,等待SurfaceFlinger合成
  4. Acquired : 被获取, SurfaceFlinger 正持有该buffer 进行合成

surface的主要作用:

  1. 获取Canvas 来干绘制的活
  2. 申请buffer,并把canvas最终生产的图形、纹理数据放进去

1.3 SurfaceFlinger

  1. 独立的一个 service, 接收所有 surface 作为输入
  2. 创建 Layer (其主要的组件是一个 BufferQueue) 与 Surface 一一对应
  3. 根据Zorder ,透明度,大小,位置等参数,计算每个layer在最终合成图像中的位置
  4. 交由HWComposer 或 OpenGL生成最终的栅格化数据
  5. 放到layer的framebuffer上

1.4 Layer

  1. surfaceflinger 进行合成的基本操作单元,主要的组件是一个 bufferqueue
  2. 在应用请求创建surface的时候在surfaceflinger内部创建
  3. 一个surface对应一个layer
  4. layer 其实是一个framebuffer ,其中有两个GraphicBuffer , FrontBuffer 和 BackBuffer

1.5 HardWare Composer

HWC 主要目标是通过可用硬件确定组合缓冲区的最有效的方式

  1. surfaceflinger 为 HWC 提供完整的 layers 的列表并询问, “ do you want handle it?"
  2. 根据硬件性能决定使用哪个? 分别将每个layer 对应标记为overlay 或 GLES composition 来进行响应
  3. SF处理需要GPU合成的layers , 将结果递交给HWC做显示(HAL), 需要硬件图层合成器合成的layers由HWC自行处理。
  4. 合成layer时,优先选择HWC ,无法解决时。SF 采用默认的3D合成,调用OpenGL 标准接口,将各layer 绘制到fb上。

两种合成方式:

  1. 离线合成 3D 合成
  2. 在线合成 Overlay 技术

img

图形数据流:

img

1.6 Screen 显示

显示屏上的内容,是从硬件帧缓冲区读取的。

读取过程:

从Buffer 的起始地址开始,从上往下,从左往右扫描整个 buffer ,将内容映射到显示屏上。

双缓冲:

一个FrontBuffer 用于提供屏幕显示内容

一个BackBuffer 用于后台合成下一帧图形

img

  1. 前一帧显示完毕,后一帧准备好了

  2. 屏幕将开始读取下一帧的内容(后缓冲区的内容)

  3. 前后缓冲区进行一次角色互换

  4. 之前的后缓冲区变为前缓冲区, 进行图形的显示

  5. 之前的前缓冲区则变为后缓冲区,进行图形的合成

官方给出的图了解关键组件如何协同工作:

img

  • Image Stream Producers: 图像流生产方可以是生成图形缓冲区以提供消费的任何内容。
  • Image Stream Consumers: 图像流最常见的消费者是 SurfaceFlinger,该系统服务会消费当前可见的 Surface,并使用 WindowManager 中提供的信息将它们合成交到 Display。 SurfaceFlinger 使用 OpenGL 和 HardWare Composer 来合成 Surface,其他OpenGL ES 应用也可以消费图像流,例如相机应用会消费相机预览图像流;非GL应用也可以是使用方,例如ImageReader 类。
  • Hardware Composer: 这是显示子系统的硬件抽象层,SurfaceFlinger 可以将某些合成工作委托给 Hardware Composer,以分担 OpenGL 和 GPU 上的工作量。SurfaceFlinger 在收集可见层的所有缓冲区之后会询问 Hardware Composer 应如何进行合成。
  • Gralloc: 使用图形内存分配器(Gralloc)来分配图像生产方请求的内存。

渲染 Android 应用视图的渲染流程总结:

  1. 测量流程用来确定视图的大小
  2. 布局流程用来确定视图的位置
  3. 绘制流程最终将视图绘制在应用窗口上
  • Android 应用程序窗口首先是使用 Canvas 通过 Skia 图形库 API 来绘制在一块画布上

  • 实际地通过surface绘制在这块画布里面的一个图形缓冲区

  • 这个图形缓冲区最终会通过layer的形式交给 surfaceflinger 来合成

  • 合成后栅格化数据的操作交由 HWC 或者 OpenGL 生成,即将这个图形缓冲区渲染到硬件帧缓冲区中,供屏幕显示。

2. CPU/GPU 的合成帧率和Display 的刷新频率同步问题

**屏幕刷新率(HZ): **(消费内容的速度)

代表屏幕在1秒内刷新屏幕的次数,android 手机一般为60Hz(一秒刷新60帧,大约16.67ms刷新一帧)

系统帧速率(FPS): (生产内容的速度)

系统在1秒内合成的帧数,该值的大小由系统算法和硬件决定。

引入三个核心元素:VSYNC 、Tripple Buffer 和 Choregrapher ,*来解决同步问题i

2.1 垂直同步(Vsync)

  1. 当屏幕从缓冲区扫描一帧到屏幕上之后,开始扫描下一帧之前,发出一个同步信号,该信号用来切换前缓冲区和后缓冲区。
  2. 该垂直同步信号让合成帧的速率以屏幕刷新率(固定不变)为标杆,系统帧速率可以改变。

CPU和GPU的分工:

  • CPU: Measure , layout, 纹理和多边形形成,发送纹理和多边形到GPU
  • GPU: 将CPU生成的纹理和多边形进行栅格化以及合成

1)没有VSYNC信号同步时

img

  1. 第一个16ms开始: Display 显示第0帧,CPU处理完第一帧后,GPU 紧接其后处理继续第一帧。三者都在正常工作。
  2. 进入第二个16ms: 因为早在上一个 16ms 时间内,第1帧以已经由CPU, GPU处理完毕。故Display可以直接显示第1帧。显示没有问题。但在本16ms期间,CPU和GPU却未及时去绘制第2帧数据(前面的空白区表示CPU和GPU忙其他的事),直到本周期快结束时,CPU/GPU才去处理第2帧数据。
  3. 进入第三个16ms: 此时Display 应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank),导致错过了显示第二帧。

2)引入VSYNC信号同步后

img

加入VSYNC信号同步以后,每收到VSYNC中断,CPU就开始处理各帧数据。

已经解决了刷新不同步的问题。

但如果CPU/GPU的帧率低于Display的刷新率,情况如下:

img

  1. 在第二个16ms时间段,Display 本应该显示B帧,但却因为GPU还在处理B帧,导致A帧被重复显示。
  2. 同理。在第二个16ms时间段内,cpu无所事事,因为A buffer被Display 在使用。 B buffer被 GPU 在使用。注意,一旦过了VSYNC时间点,CPU就不能被触发以处理绘制工作了。

Question:

  1. CPU/GPU 的帧率高于Display 的刷新率,CPU/GPU出现空闲状态。这个状态是否可以利用起来提前为下一轮准备好数据呢?
  2. 为什么CPU不能在第二个16ms处开始绘制工作呢?因为只有2个buffer

2.2 Tripple Buffer

针对以上问题,google给出的解决方案: 加入第3个Buffer

CPU 和 GPU 还有 SurfaceFlinger 各占一个 Buffer ,并行处理图形。

img

上图中,第二个16ms时间段,CPU使用C buffer 绘图。虽然还是会多显示A帧一次,但后续显示就比较流畅了。

总结:

  1. 一个Buffer:如果在 同一个buffer进行读取和写入(合成)操作,将会导致屏幕显示多帧内容,出现图像撕裂现象。

  2. 两个buffer: 解决撕裂问题,但是有卡顿问题,而且cpu/Gpu利用率不高

  3. 三个buffer: 提高cpu/Gpu利用率,减少卡顿,但是引入了延迟问题(三缓冲)

2.3 Choreographer

屏幕的Vsync 信号只是用来控制帧缓冲区的切换,并未控制上层的绘制节奏,也就是说上层的生产节奏和屏幕的显示节奏是脱离的:

img

Vsync 信号如果光是切换buffer,而不及时通知上层开始CPU GPU的工作,那么显示内容的合成还是无法同步的。

所以google 加入了上层接收 垂直同步 信号的逻辑

img

那么上层是如何接收到这个Vsync 消息的呢?

img

google 为上层设计了一个 Choreographer 类,作为VSYNC信号的上层接收者。

Choreographer 需要向 surfaceFlinger 来注册一个 vsync 信号的接收器 DisplayEventReceiver.

同时Choregrapher 的内部维护了一个 CallbackQueue,用来保存上层关心Vsync信号的组件,包括ViewRootImpl, TextView, ValueAnimator等。

上层接收Vsync的时序图:

img

img

一般,上层需要绘制的新的UI都是因为View 的 requestLayout 或者是 invalidate 方法被调用触发的。

  1. requestLayout 或者 invalidate 触发更新视图请求

  2. 更新请求传递到 ViewRootImpl 中,ViewRootImpl 向主线程 MessageQueue 中加入一个阻塞器,拦截所有同步消息。此时,我们再通过Handler 向主线程MessageQueue 发送的所有Message 都将无法执行。

  3. ViewRootImpl 向 Choreographer 注册一个 Vsync 信号

  4. Choreographer 通过 DisplayEventReceiver 向 framework 层注册一个 Vsync 信号

  5. 当底层产生一个Vsync 消息时,该信号将会发送给 DisplayEventReceicer,最后传递给Choreographer.

  6. Choreographer 收到Vsync信号之后,向主线程MessageQueue 发送一个异步消息,不会被拦截。

  7. 最后,异步消息的执行者是viewRootImpl, 也就是真正开始绘制下一帧了。