苹果的渲染框架(通俗的理解)
根据日常开发和上图分析渲染流程
- UIKit: 开发中使用的用户交互组件都来自于 UIKit
(理解为收集渲染信息+用户交互事件信息的框架)
- 提供各种 UI 组件
- 提供配置 UI 组件的样式接口(autoLayout, Frame, Color, Text ……),交由 Core Animation 处理
- 封装用户事件接口
- Core Animation:字面翻译是核心动画,看下 CALayer,
CA
表示的就是 Core Animation,Core Animation 理解为收集渲染信息,触底给底部然后得到一个渲染结果 contents
(UIKit 上所有能看到的东西都是通过layer.contents
呈现的)- 给 UIKit 提供 layer
- 管理动画
- 收集渲染数据交由渲染引擎处理
- 不负责用户事件相关处理!
- OpenGL ES & Core Graphics 渲染引擎
- 收集 Core Animation 提供的渲染数据
- Core Graphics 是基于 Quartz(轻量级 2D 渲染) 高级绘图引擎
- Graphics Hardware 译为图形硬件,也就是我们经常提及的 GPU
- GPU 的高度并行结构使其在大块数据并行处理的算法中比通用 CPU 更有效
用户操作的核心在 Core Animation,你要渲染的数据都要放在(layer.contents
)上
当然用户也可以直接访问
- OpenGL ES(GLKView) 来处理渲染
- Core Graphic (CG-- 相关接口) 来生成 bitmap 数据,然后将其放到 Core Animation 上
layer.contents
- UIKit 中的组件都会关联到相应的 CALayer
- 渲染引擎 OpenGL ES & Core Graphic 都会把渲染结果交给 layer.contents (CGImage)
用户使用渲染框架的数据流向
官方渲染流程 Core Animation Pipeline
在看完 wwdc2014 session419(Advanced Graphics and Animations for iOS Apps) 有了更深入的了解
上图需要注意:
- 苹果的 UI 渲染频率是 60hz 16.67ms 一次(Vsync)
- 每个垂直的虚线表示一个 Vsync
- 水平虚线,表示一个硬件资源
问题来了,根据上图一个渲染周期需要 3frame,那么真实的渲染频率只有 20hz
那么系统是怎么做到 60hz 的呢?答案: 流水线
如果在每一帧上对应硬件操作都完成了,那么就会以 60hz 的速度渲染
接下来说一下渲染过程中的每个细节部分
提交事务(commit transaction)
主要是 4个阶段
- Layout:构建 Views
- 调用
layoutSubviews
如果重载了 - 创建 view,addSubView:
- 填充内容,轻量级的数据查询(就是 string 赋值之类的)
- 通常是 CPU, I/O 负责
- 调用
- Display:绘制 Views
drawRect
绘制内容,如果重载了(主要使用 Core Graphic,避免执行复杂操作)- String drawing
- 通常是 CPU / memory 负责
- Prepare:做些 Core Animation 相关操作
- image decoding(view hierachy 绘制的,jpeg,png)
- image conversion(因为有些图片 GPU 不支持),通常是解码成 bitmap
- Commit:打包 layers,然后将他们发给 render server
- 递归上述流程
- 如果 layer 树很复杂,那么会很耗性能。所以尽可能的让 layer tree 平一些(少几层)
动画Animation
主要是 3个阶段,有 2个阶段发生在 Application 进程中,1个在 Render Server 进程中
- 创建动画接着更新视图层级
animateWithDuration:animations:
- 准备动画,然后提交
layoutSubviews
,drawRect:
就是提交事务这几步 - 使用进程间通信,render server 进程绘制出动画相关的每一帧,在交由 App 进程
渲染 render
一个 view 的渲染过程
添加 masking 后的渲染过程
GPU 的离屏渲染,就是对于一个 view 需要多次渲染组合,因为需要多个渲染层所以需要离屏渲染开辟缓存,绘制这些mask,radius,blend ……
如果需要组合的那些渲染过程在 CPU 中完成,然后CPU直接把一个绘制好的 image 交给 GPU 渲染就不会有这些问题了!不过 CPU 性能问题!drawRect 中如果绘制 image 太过复杂依然会出现掉帧问题
Runloop & Core Animation
RunLoop主要处理以下6类事件:
1 | static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(); |
- Observer事件:runloop中状态变化时进行通知。Core Animation 监听 RunloopObserver 闲置的时候触发。
- Block事件:
- Main_Dispatch_Queue事件:GCD中dispatch到main queue的block 会在 main loop 中执行。
- Timer事件:延迟的NSObject PerformSelector,延迟的dispatch_after,timer事件。
- Source0事件:处理如UIEvent,CFSocket这类事件。需要手动触发。触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。(用户可以手动调用performSelector 方法触发 source0)
- Source1事件:处理系统内核的mach_msg事件。(推测CADisplayLink也是这里触发)。
App 进程的 Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件(就是不让 runloop 睡!)
。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。
当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。
布局&渲染
渲染图片之前一定要先计算好尺寸位置,也就是 frame,AutoLayout 最终结果也是 frame,在 iOS12以后 AutoLayout 性能大幅提升
更新布局限制的过程是:子–>父(super.updateConstraints最后调用)
更新布局的过程是:父–>子 (layoutSubview)
布局渲染相关方法
- 布局限制
autulayout 的布局限制不要将布局 放到 updateContraints
方法中,这里是放大量布局更新的地方,通常布局代码放在 view 的 init
, awakeFromNib
or viewcontroller 的 viewDidLoad,loadView
方法中
setNeedXXXX 是标记脏布局,在下一次 RunLoop循环的时候就会调用 updateXXX 方法
- 布局
layoutSubViews
被系统调用的时候,所有相关的子view的 frame 都已经被 AutoLayout 的布局引擎布局好了,都有了自己 frame,这个时候可以更改 他们的frame了
- 显示(CPU)
嗯以上的1~3都是 CPU的操作
- drawRect方法 通过 CoreGraphic库绘制 2D image,放在 layer.contents 编码交给 Render Server处理
- 同理 CALayer 的 drawLayer 方法
- CALayer 的 delegate
1 | @protocol CALayerDelegate <NSObject> |
默认情况 UIView 是 CALayer 的CALayerDelegate,drawRect 内部就是调用 CALayerDelegate的方法 给 Layer 绘制 contents
只要 layer.contents 的东西交由 Render Server,内部render 引擎渲染就是系统的事了
所以有了写异步渲染框架,只要程序员在子线程配置好了 contents然后在主线程交给 layer 就可以了
离屏渲染
离屏渲染是 GPU 为了缓存的已经渲染出来图形,等待跟其他图形组合
离屏渲染空间只有屏幕的 2.5倍
eg: 圆角图片
由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染。但如果仅仅是实现一个简单的效果,直接使用 CPU 渲染的效率又可能比离屏渲染好,毕竟普通的离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。对一些简单的绘制过程来说,这个过程有可能用CoreGraphics,全部用CPU来完成反而会比GPU做得更好。