Apple 系统界面渲染过程

文章目录
  1. 1. 苹果的渲染框架(通俗的理解)
    1. 1.1. 用户使用渲染框架的数据流向
  2. 2. 官方渲染流程 Core Animation Pipeline
    1. 2.1. 提交事务(commit transaction)
    2. 2.2. 动画Animation
    3. 2.3. 渲染 render
  3. 3. Runloop & Core Animation
    1. 3.1. 布局&渲染
    2. 3.2. 布局渲染相关方法
  4. 4. 离屏渲染

苹果的渲染框架(通俗的理解)

coreAnimation

根据日常开发和上图分析渲染流程

  1. UIKit: 开发中使用的用户交互组件都来自于 UIKit (理解为收集渲染信息+用户交互事件信息的框架)
    1. 提供各种 UI 组件
    2. 提供配置 UI 组件的样式接口(autoLayout, Frame, Color, Text ……),交由 Core Animation 处理
    3. 封装用户事件接口
  2. Core Animation:字面翻译是核心动画,看下 CALayer,CA 表示的就是 Core Animation,Core Animation 理解为收集渲染信息,触底给底部然后得到一个渲染结果 contents(UIKit 上所有能看到的东西都是通过 layer.contents 呈现的)
    1. 给 UIKit 提供 layer
    2. 管理动画
    3. 收集渲染数据交由渲染引擎处理
    4. 不负责用户事件相关处理!
  3. OpenGL ES & Core Graphics 渲染引擎
    1. 收集 Core Animation 提供的渲染数据
    2. Core Graphics 是基于 Quartz(轻量级 2D 渲染) 高级绘图引擎
  4. Graphics Hardware 译为图形硬件,也就是我们经常提及的 GPU
    1. GPU 的高度并行结构使其在大块数据并行处理的算法中比通用 CPU 更有效

用户操作的核心在 Core Animation,你要渲染的数据都要放在(layer.contents)上
当然用户也可以直接访问

  • OpenGL ES(GLKView) 来处理渲染
  • Core Graphic (CG-- 相关接口) 来生成 bitmap 数据,然后将其放到 Core Animation 上layer.contents
  1. UIKit 中的组件都会关联到相应的 CALayer
  2. 渲染引擎 OpenGL ES & Core Graphic 都会把渲染结果交给 layer.contents (CGImage)

用户使用渲染框架的数据流向

渲染的数据流向

官方渲染流程 Core Animation Pipeline

在看完 wwdc2014 session419(Advanced Graphics and Animations for iOS Apps) 有了更深入的了解

core animation pipeline

上图需要注意:

  1. 苹果的 UI 渲染频率是 60hz 16.67ms 一次(Vsync)
  2. 每个垂直的虚线表示一个 Vsync
  3. 水平虚线,表示一个硬件资源

问题来了,根据上图一个渲染周期需要 3frame,那么真实的渲染频率只有 20hz
那么系统是怎么做到 60hz 的呢?答案: 流水线

core-animation-flow

如果在每一帧上对应硬件操作都完成了,那么就会以 60hz 的速度渲染

接下来说一下渲染过程中的每个细节部分

提交事务(commit transaction)

主要是 4个阶段

  1. Layout:构建 Views
    1. 调用 layoutSubviews 如果重载了
    2. 创建 view,addSubView:
    3. 填充内容,轻量级的数据查询(就是 string 赋值之类的)
    4. 通常是 CPU, I/O 负责
  2. Display:绘制 Views
    1. drawRect 绘制内容,如果重载了(主要使用 Core Graphic,避免执行复杂操作)
    2. String drawing
    3. 通常是 CPU / memory 负责
  3. Prepare:做些 Core Animation 相关操作
    1. image decoding(view hierachy 绘制的,jpeg,png)
    2. image conversion(因为有些图片 GPU 不支持),通常是解码成 bitmap
  4. Commit:打包 layers,然后将他们发给 render server
    1. 递归上述流程
    2. 如果 layer 树很复杂,那么会很耗性能。所以尽可能的让 layer tree 平一些(少几层)

动画Animation

主要是 3个阶段,有 2个阶段发生在 Application 进程中,1个在 Render Server 进程中

  1. 创建动画接着更新视图层级 animateWithDuration:animations:
  2. 准备动画,然后提交 layoutSubviews, drawRect: 就是提交事务这几步
  3. 使用进程间通信,render server 进程绘制出动画相关的每一帧,在交由 App 进程

Animation 过程

渲染 render

一个 view 的渲染过程

render pass

添加 masking 后的渲染过程

render-pass-mask

GPU 的离屏渲染,就是对于一个 view 需要多次渲染组合,因为需要多个渲染层所以需要离屏渲染开辟缓存,绘制这些mask,radius,blend ……
如果需要组合的那些渲染过程在 CPU 中完成,然后CPU直接把一个绘制好的 image 交给 GPU 渲染就不会有这些问题了!不过 CPU 性能问题!drawRect 中如果绘制 image 太过复杂依然会出现掉帧问题

Runloop & Core Animation

RunLoop主要处理以下6类事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

1. Observer
2. block
3. timer
4. main_dispatch_queue
5. source0
6. source1
  1. Observer事件:runloop中状态变化时进行通知。Core Animation 监听 RunloopObserver 闲置的时候触发。
  2. Block事件:
  3. Main_Dispatch_Queue事件:GCD中dispatch到main queue的block 会在 main loop 中执行。
  4. Timer事件:延迟的NSObject PerformSelector,延迟的dispatch_after,timer事件。
  5. Source0事件:处理如UIEvent,CFSocket这类事件。需要手动触发。触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。(用户可以手动调用performSelector 方法触发 source0)
  6. 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 性能大幅提升

layout&render

更新布局限制的过程是:子–>父(super.updateConstraints最后调用)
更新布局的过程是:父–>子 (layoutSubview)

布局渲染相关方法

layout&render-function

  1. 布局限制

autulayout 的布局限制不要将布局 放到 updateContraints 方法中,这里是放大量布局更新的地方,通常布局代码放在 view 的 init, awakeFromNib or viewcontroller 的 viewDidLoad,loadView 方法中

setNeedXXXX 是标记脏布局,在下一次 RunLoop循环的时候就会调用 updateXXX 方法

  1. 布局

layoutSubViews 被系统调用的时候,所有相关的子view的 frame 都已经被 AutoLayout 的布局引擎布局好了,都有了自己 frame,这个时候可以更改 他们的frame了

  1. 显示(CPU)

嗯以上的1~3都是 CPU的操作

  • drawRect方法 通过 CoreGraphic库绘制 2D image,放在 layer.contents 编码交给 Render Server处理
  • 同理 CALayer 的 drawLayer 方法
  • CALayer 的 delegate
1
2
3
4
5
6
7
8
9
10
11
12
13
@protocol CALayerDelegate <NSObject>
@optional

/* If defined, called by the default implementation of the -display
* method, in which case it should implement the entire display
* process (typically by setting the `contents' property). */

- (void)displayLayer:(CALayer *)layer;

/* If defined, called by the default implementation of -drawInContext: */

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
@end

默认情况 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做得更好。