mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
190 lines
11 KiB
Markdown
190 lines
11 KiB
Markdown
# iOS 界面渲染流程
|
||
|
||
> 下面几个问题你熟悉吗?
|
||
>
|
||
> - 为什么调用 `[UIView serNeedsDisplay]` 并没有立刻发生当前视图的绘制工作?
|
||
|
||
|
||
|
||
## 视图显示原理
|
||
|
||
为什么调用 `[UIView setNeedsDisplay]` 并没有立刻发生当前视图的绘制工作?
|
||
|
||
UIView 绘制流程。
|
||
|
||
<img src="./../assets/UIViewRefreshProcess.png" style="zoom:60%" />
|
||
|
||
|
||
|
||
当调用 UIView `[UIView setNeedsDisplay]` 方法时,系统会立刻调用其 Layer 的同名方法 `[view.layer setNeedsDisplay]` 方法,之后相当于给当前 Layer 打上一个脏标记,之后会在当前 RunLoop 快要结束的时候才会调用 Layer 的 `[CALayer display]` 方法。然后进入当前 UIView 真正的绘制流程中。
|
||
|
||
其次,会判断 CALayer 的代理,有没有实现 `displayLayer:` 方法,如果没有实现,则进入系统的绘制流程中;如果实现了,则可能是异步绘制或者自定义渲染的实现。
|
||
|
||
Tips:`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setNeedsDisplay]` 方法。要么手动触发 `[view.layer setNeedsDisplay]` 要么,调用一下 `-(void)drawRect:(CGRect)rect` 方法(即使是空实现也没关系)。
|
||
|
||
下面来个 Demo 展示下简单的异步绘制一个 String。
|
||
|
||
<img src="./../assets/AsyncUILabelRender.png" style="zoom:30%" />
|
||
|
||
|
||
|
||
|
||
|
||
接下来看看系统的绘制实现流程:
|
||
|
||
<img src="./../assets/UIViewSystemRenderProcess.png" style="zoom:60%" />
|
||
|
||
如何实现异步绘制?
|
||
|
||
`[layer.delegate displayPlayer:]`
|
||
|
||
- 代理负责生成对应的 bitmap
|
||
- 设置该 bitmap 作为 layer.contents 属性的值
|
||
|
||
|
||
|
||
<img src='./../assets/AsyncRenderProcessAPI.png' style="zoom:30%" />
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
## 渲染机制
|
||
|
||

|
||
|
||
iOS 渲染框架可以分为4层,顶层是 UIKit,包括图形界面的高级 API 和常用的各种 UI 控件。UIKit 下层是 Core Animation,不要被名字误解了,它不光是处理动画相关,也在做图形渲染相关的事情(比如 UIView 的 CALayer 就处于 Core Animation 中)。Core Animation 之下就是由 OpenGL ES 和 CoreGraphics 组成的图形渲染层,OpenGL ES 主要操作 GPU 进行图形渲染,CoreGraphics 主要操作 CPU 进行图形渲染。上面3层都属于渲染图形软件层,再下层就是图形显示硬件层。
|
||
|
||
iOS 图形界面的显示是一个复杂的流程,一部分数据通过 Core Graphics、Core Image 有 CPU 预处理,最终通过 OpenGL ES 将数据传输给 GPU,最终显示到屏幕上。
|
||
|
||

|
||
|
||
- Core Animation 提交会话(事务),包括自己和子树(view hierarchy) 的布局状态
|
||
|
||
- Render Server 解析所提交的子树状态,生成绘制指令
|
||
|
||
- GPU 执行绘制指令
|
||
|
||
- 显示器显示渲染后的数据
|
||
|
||
## Core Animation
|
||
|
||

|
||
|
||
可以看到 Core Animation pipeline 由4部分组成:Application 层的 Core Animation 部分、Render Server 中的 Core Animation 部分、GPU 渲染、显示器显示。
|
||
|
||
### Application 层 Core Animation 部分
|
||
|
||

|
||
|
||
- 布局(Layout):`layoutSubviews`、`addSubview`,这里通常是 CPU、IO 繁忙
|
||
|
||
- 显示(Display):调用 view 重写的 `drawRect` 方法,或者绘制字符串。这里主要是 CPU 繁忙、消费较多内存。每个 UIView 都有 CALayer,同时图层又一个像素存储控件,存储视图,调用 `setNeedsDisplay` 仅会设置图层为 dirty。当渲染系统准备就绪,调用视图的 `display` 方法,同时装配像素存储空间,建立一个 Core Graphics 上下文(CGContextRef),将上下文 push 进上下文堆栈,绘图程序进入对应的内存存储空间。
|
||
|
||
- 准备(Prepare):图片解码、图片格式转换。GPU 不支持某些图片格式,尽量使用 GPU 能支持的图片格式
|
||
|
||
- 提交(Commit):打包 layers 并发送给 Render Server,递归提交子树的 layers。如果子树层级较多(复杂),则对性能造成影响
|
||
|
||
### Render Server 中 Core Animation 部分
|
||
|
||
Render Server 是一个独立的渲染进程,当收到来自 Application 的 (IPC) 事务时,首先解析 layer 层级关系,然后 Decode。最后执行 Draw Calls(执行对应的 OpenGL ES 命令)
|
||
|
||
### GPU 渲染
|
||
|
||
- OpenGL ES 的 command buffer 进行定点变换,三角形拼接、光栅话变为 parameter buffer
|
||
|
||
- parameter buffer 进行像素变化,testing、blending 生成 frame buffe
|
||
|
||
### 显示器显示
|
||
|
||
视频控制器从 frame buffer 中读取数据显示在显示屏上。
|
||
|
||
## UIView 绘制流程
|
||
|
||

|
||
|
||
- 每个 UIView 都有一个 CALayer,layer 属性都有 contents,contents 其实是一块缓存,叫做 backing store
|
||
|
||
- 当 UIView 被绘制时,CPU 执行 drawRect 方法,通过 context 将数据写入 backing store 中(位图 bitmap)
|
||
|
||
- 当 backing store 写完后,通过 Render Server 交给 GPU 去渲染,最后显示到屏幕上
|
||
|
||

|
||
|
||
- 调用 `[UIView setNeedsDisplay]` 方法时,并没有立即执行绘制工作,而是马上调用 `[view.layer setNeedsDisplay]` 方法,给当前 layer 打上标记
|
||
|
||
- 在当前 RunLoop 快要结束的时候调用 layer 的 display 方法,来进入到当前视图真正的绘制流程
|
||
|
||
- 在 layer 的 display 方法内部,系统会判断 layer 的 layer.delegate 是否实现了 `displayLayer` 方法
|
||
|
||
- 如果没有,则执行系统的绘制流程
|
||
|
||
- 如果实现了,则会进入异步绘制流程
|
||
|
||
- 最后把绘制完的 backing store 提交给 GPU
|
||
|
||
### 系统绘制流程
|
||
|
||

|
||
|
||
- 首先 CALayer 内部会创建一个 CGContextRef,在 drwaRect 方法中,可以通过上下文堆栈取出 context,拿到当前视图渲染上下文也就是 backing store
|
||
|
||
- 然后 layer 会判断是否存在代理,若没有,则调用 CALayer 的 drawInContext
|
||
|
||
- 如果存在代理,则调用代理方法。然后做当前视图的绘制工作,然后调用 view 的 drawRect 方法
|
||
|
||
- 最后由 CALayer 上传对应的 backing store(可以理解为位图)提交给 GPU。
|
||
|
||
### 异步绘制流程
|
||
|
||

|
||
|
||
- 如果 layer 有代理对象,且代理对象实现了代理方法,则可以进入异步绘制流程
|
||
|
||
- 异步绘制流程中主要生成对应的 bitmap。目的是最后一步,需要将 bitmap 设置为 layer.contents 的值
|
||
|
||
- 左侧是主队列,右侧是全局并发队列
|
||
|
||
- 调用了setNeedsDiaplay 方法后,在当前 Runloop 将要结束的时候,会有系统调用视图所对应 layer 的 display 方法
|
||
|
||
- 通过在子线程中去做位图的绘制,此时主线程可以去做些其他的工作。在子线程中:主要通过 CGBitmapContextCreate 方法,来创建一个位图的上下文、通过CoreGraphic API,绘制 UI、通过 CGBitmapContextCreatImage 方法,根据所绘制的上下文,生成一张 CGImage 图片
|
||
|
||
- 然后再回到主队列中,提交这个位图,设置给 CALayer 的 contents 属性
|
||
|
||
## 图片加载库都做了什么事
|
||
|
||
众所周知,iOS应用的渲染模式,是完全基于Core Animation和CALayer的(macOS上可选,另说)。因此,当一个UIImageView需要把图片呈现到设备的屏幕上时候,其实它的Pipeline是这样的:
|
||
|
||
1. 一次Runloop完结 ->
|
||
2. Core Animation提交渲染树CA::render::commit ->
|
||
3. 遍历所有Layer的contents ->
|
||
4. UIImageView的contents是CGImage ->
|
||
5. 拷贝CGImage的Bitmap Buffer到Surface(Metal或者OpenGL ES Texture)上 ->
|
||
6. Surface(Metal或者OpenGL ES)渲染到硬件管线上
|
||
|
||
这个流程看起来没有什么问题,但是注意,Core Animation库自身,虽然支持异步线程渲染(在macOS上可以手动开启),但是UIKit的这套内建的pipeline,全部都是发生在主线程的。
|
||
|
||
因此,当一个CGImage,是采取了惰性解码(通过Image/IO生成出来的),那么将会在主线程触发先前提到的惰性解码callback(实际上Core Animation的调用,触发了一个`CGDataProviderRetainBytePtr`),这时候Image/IO的具体解码器,会根据先前的图像元信息,去分配内存,创建Bitmap Buffer,这一步骤也发生在主线程。
|
||
|
||
这个流程带来的问题在于,主线程过多的频繁操作,会造成渲染帧率的下降。实验可以看出,通过原生这一套流程,对于一个1000*1000的PNG图片,第一次滚动帧率大概会降低5-6帧(iPhone 5S上当年有人的测试)。后续帧率不受影响,因为是惰性解码,解码完成后的Bitmap Buffer会复用。
|
||
|
||
所以,最早不知是哪个团队的人(可能是[FastImageCache](https://github.com/path/FastImageCache),不确定)发现,并提出了另一种方案:通过预先调用获取Bitmap,强制Image/IO产生的CGImage解码,这样到最终渲染的时候,主线程就不会触发任何额外操作,带来明显的帧率提升。后面的一系列图片库,都互相效仿,来解决这个问题。
|
||
|
||
具体到解决方案上,目前主流的方式,是通过CGContext开一个额外的画布,然后通过`CGContextDrawImage`来画一遍原始的空壳CGImage,由于在`CGContextDrawImage`的执行中,会触发到`CGDataProviderRetainBytePtr`,因此这时候Image/IO就会立即解码并分配Bitmap内存。得到的产物用来真正产出一个CGImage-based的UIImage,交由UIImageView渲染。
|
||
|
||
## ForceDecode的优缺点
|
||
|
||
上面解释了ForceDecode具体解决的问题,当然,这个方案肯定存在一定的问题,不然苹果研发团队早已经改变了这套Pipeline流程了
|
||
|
||
优点:可以提升,图像第一次渲染到屏幕上时候的性能和滚动帧率
|
||
|
||
缺点:提前解码会立即分配Bitmap Buffer的内存,增加了内存压力。举例子对于一张大图(2048*2048像素,32位色)来说,就会立即分配16MB(2048 * 2048 * 4 Bytes)的内存。
|
||
|
||
由此可见,这是一个拿空间换时间的策略。但是实际上,iOS设备早期的内存都是非常有限的,UIKit整套渲染机制很多地方采取的都是时间换空间,因此最终苹果没有使用这套Pipeline,而是依赖于高性能的硬件解码器+其他优化,来保证内存开销稳定。当然,作为图片库和开发者,这就属于仁者见仁的策略了。如大量小图渲染的时候,开启Force Decode能明显提升帧率,同时内存开销也比较稳定。
|