Buffer1 : 图形缓冲区
Buffer2: 硬件帧缓冲区
1. 图形渲染流程
1.1 app层绘制
由ViewRootImpl 发起 performTraversals 开始 View 的绘制
- 测量View的宽高(Measure)
- 设置View的宽高位置(Layout)
- 创建显示列表,并执行绘制(Draw)
- 绘制通过图形处理引擎来实现,生成多变形和纹理(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供上层使用。
surface对生产者代理对象,surface (native)对应生产者本地对象。
流程:
-
上层app通过surface获取buffer,供上层绘制,绘制过程通过canvas来完成,底层实现是skia引擎。
-
绘制完成后数据通过surface被queue进BufferQueue
-
然后监听会通知surfaceflinger去消费buffer
-
接着surfaceFlinger就acquie数据拿去合成
-
合成完成后将 buffer release 回bufferqueue
-
如此循环,形成一个buffer被循环使用的过程
状态:
- free : 可被上层使用
- Dequeued : 出列,正在被上层使用
- Queued: 入列,已被上层绘制,等待SurfaceFlinger合成
- Acquired : 被获取, SurfaceFlinger 正持有该buffer 进行合成
surface的主要作用:
- 获取Canvas 来干绘制的活
- 申请buffer,并把canvas最终生产的图形、纹理数据放进去
1.3 SurfaceFlinger
- 独立的一个 service, 接收所有 surface 作为输入
- 创建 Layer (其主要的组件是一个 BufferQueue) 与 Surface 一一对应
- 根据Zorder ,透明度,大小,位置等参数,计算每个layer在最终合成图像中的位置
- 交由HWComposer 或 OpenGL生成最终的栅格化数据
- 放到layer的framebuffer上
1.4 Layer
- surfaceflinger 进行合成的基本操作单元,主要的组件是一个 bufferqueue
- 在应用请求创建surface的时候在surfaceflinger内部创建
- 一个surface对应一个layer
- layer 其实是一个framebuffer ,其中有两个GraphicBuffer , FrontBuffer 和 BackBuffer
1.5 HardWare Composer
HWC 主要目标是通过可用硬件确定组合缓冲区的最有效的方式
- surfaceflinger 为 HWC 提供完整的 layers 的列表并询问, “ do you want handle it?"
- 根据硬件性能决定使用哪个? 分别将每个layer 对应标记为overlay 或 GLES composition 来进行响应
- SF处理需要GPU合成的layers , 将结果递交给HWC做显示(HAL), 需要硬件图层合成器合成的layers由HWC自行处理。
- 合成layer时,优先选择HWC ,无法解决时。SF 采用默认的3D合成,调用OpenGL 标准接口,将各layer 绘制到fb上。
两种合成方式:
- 离线合成 3D 合成
- 在线合成 Overlay 技术
图形数据流:
1.6 Screen 显示
显示屏上的内容,是从硬件帧缓冲区读取的。
读取过程:
从Buffer 的起始地址开始,从上往下,从左往右扫描整个 buffer ,将内容映射到显示屏上。
双缓冲:
一个FrontBuffer 用于提供屏幕显示内容
一个BackBuffer 用于后台合成下一帧图形
-
前一帧显示完毕,后一帧准备好了
-
屏幕将开始读取下一帧的内容(后缓冲区的内容)
-
前后缓冲区进行一次角色互换
-
之前的后缓冲区变为前缓冲区, 进行图形的显示
-
之前的前缓冲区则变为后缓冲区,进行图形的合成
官方给出的图了解关键组件如何协同工作:
- 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 应用视图的渲染流程总结:
- 测量流程用来确定视图的大小
- 布局流程用来确定视图的位置
- 绘制流程最终将视图绘制在应用窗口上
-
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)
- 当屏幕从缓冲区扫描一帧到屏幕上之后,开始扫描下一帧之前,发出一个同步信号,该信号用来切换前缓冲区和后缓冲区。
- 该垂直同步信号让合成帧的速率以屏幕刷新率(固定不变)为标杆,系统帧速率可以改变。
CPU和GPU的分工:
- CPU: Measure , layout, 纹理和多边形形成,发送纹理和多边形到GPU
- GPU: 将CPU生成的纹理和多边形进行栅格化以及合成
1)没有VSYNC信号同步时
- 第一个16ms开始: Display 显示第0帧,CPU处理完第一帧后,GPU 紧接其后处理继续第一帧。三者都在正常工作。
- 进入第二个16ms: 因为早在上一个 16ms 时间内,第1帧以已经由CPU, GPU处理完毕。故Display可以直接显示第1帧。显示没有问题。但在本16ms期间,CPU和GPU却未及时去绘制第2帧数据(前面的空白区表示CPU和GPU忙其他的事),直到本周期快结束时,CPU/GPU才去处理第2帧数据。
- 进入第三个16ms: 此时Display 应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank),导致错过了显示第二帧。
2)引入VSYNC信号同步后
加入VSYNC信号同步以后,每收到VSYNC中断,CPU就开始处理各帧数据。
已经解决了刷新不同步的问题。
但如果CPU/GPU的帧率低于Display的刷新率,情况如下:
- 在第二个16ms时间段,Display 本应该显示B帧,但却因为GPU还在处理B帧,导致A帧被重复显示。
- 同理。在第二个16ms时间段内,cpu无所事事,因为A buffer被Display 在使用。 B buffer被 GPU 在使用。注意,一旦过了VSYNC时间点,CPU就不能被触发以处理绘制工作了。
Question:
- CPU/GPU 的帧率高于Display 的刷新率,CPU/GPU出现空闲状态。这个状态是否可以利用起来提前为下一轮准备好数据呢?
- 为什么CPU不能在第二个16ms处开始绘制工作呢?因为只有2个buffer
2.2 Tripple Buffer
针对以上问题,google给出的解决方案: 加入第3个Buffer
CPU 和 GPU 还有 SurfaceFlinger 各占一个 Buffer ,并行处理图形。
上图中,第二个16ms时间段,CPU使用C buffer 绘图。虽然还是会多显示A帧一次,但后续显示就比较流畅了。
总结:
-
一个Buffer:如果在 同一个buffer进行读取和写入(合成)操作,将会导致屏幕显示多帧内容,出现图像撕裂现象。
-
两个buffer: 解决撕裂问题,但是有卡顿问题,而且cpu/Gpu利用率不高
-
三个buffer: 提高cpu/Gpu利用率,减少卡顿,但是引入了延迟问题(三缓冲)
2.3 Choreographer
屏幕的Vsync 信号只是用来控制帧缓冲区的切换,并未控制上层的绘制节奏,也就是说上层的生产节奏和屏幕的显示节奏是脱离的:
Vsync 信号如果光是切换buffer,而不及时通知上层开始CPU GPU的工作,那么显示内容的合成还是无法同步的。
所以google 加入了上层接收 垂直同步 信号的逻辑
那么上层是如何接收到这个Vsync 消息的呢?
google 为上层设计了一个 Choreographer 类,作为VSYNC信号的上层接收者。
Choreographer 需要向 surfaceFlinger 来注册一个 vsync 信号的接收器 DisplayEventReceiver.
同时Choregrapher 的内部维护了一个 CallbackQueue,用来保存上层关心Vsync信号的组件,包括ViewRootImpl, TextView, ValueAnimator等。
上层接收Vsync的时序图:
一般,上层需要绘制的新的UI都是因为View 的 requestLayout 或者是 invalidate 方法被调用触发的。
-
requestLayout 或者 invalidate 触发更新视图请求
-
更新请求传递到 ViewRootImpl 中,ViewRootImpl 向主线程 MessageQueue 中加入一个阻塞器,拦截所有同步消息。此时,我们再通过Handler 向主线程MessageQueue 发送的所有Message 都将无法执行。
-
ViewRootImpl 向 Choreographer 注册一个 Vsync 信号
-
Choreographer 通过 DisplayEventReceiver 向 framework 层注册一个 Vsync 信号
-
当底层产生一个Vsync 消息时,该信号将会发送给 DisplayEventReceicer,最后传递给Choreographer.
-
Choreographer 收到Vsync信号之后,向主线程MessageQueue 发送一个异步消息,不会被拦截。
-
最后,异步消息的执行者是viewRootImpl, 也就是真正开始绘制下一帧了。