iOS绘制与渲染--渲染流程

视图渲染框架

UIKit是常用的框架,显示、动画都通过CoreAnimation。CoreAnimation是核心动画,依赖于OpenGL ES做GPU渲染,CoreGraphics做CPU渲染;最底层的GraphicsHardWare是图形硬件。

iOS绘制与渲染--渲染流程_第1张图片

下图是另外一种表现的形式。在屏幕上显示视图,需要CPU和GPU一起协作。一部数据通过CoreGraphics、CoreImage由CPU预处理。最终通过OpenGL ES将数据传送到 GPU,最终显示到屏幕。
CoreImage支持CPU、GPU两种处理模式。

iOS绘制与渲染--渲染流程_第2张图片

显示逻辑

1、CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;
2、RenderServer解析提交的子树状态,生成绘制指令;
3、GPU执行绘制指令;
4、显示渲染后的数据;

iOS绘制与渲染--渲染流程_第3张图片

提交流程(以动画为例)

第2步为prepare to commit animation (layoutSubviews,drawRect:);

iOS绘制与渲染--渲染流程_第4张图片

1、布局(Layout)

调用layoutSubviews方法;调用addSubview:方法;

会造成CPU和I/O瓶颈;

2、显示(Display)

通过drawRect绘制视图;绘制string(字符串);

会造成CPU和内存瓶颈;每个UIView都有CALayer,同时图层有一个像素存储空间,存放视图;调用-setNeedsDisplay的时候,仅会设置图层为dirty。当渲染系统准备就绪,调用视图的-display方法,同时装配像素存储空间,建立一个CoreGraphics上下文(CGContextRef),将上下文push进上下文堆栈,绘图程序进入对应的内存存储空间。

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 10)];
[path addLineToPoint:CGPointMake(20, 20)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];

在-drawRect方法中实现如上代码,UIKit会将自动生成的CGContextRef 放入上下文堆栈。当绘制完成后,视图的像素会被渲染到屏幕上;当下次再次调用视图的-setNeedsDisplay,将会再次调用-drawRect方法。

3、准备提交(Prepare)

解码图片;图片格式转换;

GPU不支持的某些图片格式,尽量使用GPU能支持的图片格式;

4、提交(Commit)

打包layers并发送到渲染server;递归提交子树的layers;

如果子树太复杂,会消耗很大,对性能造成影响;
尽可能简化viewTree;

当显示一个UIImageView时,Core Animation会创建一个OpenGL ES纹理,并确保在这个图层中的位图被上传到对应的纹理中。当你重写-drawInContext
方法时,Core Animation会请求分配一个纹理,同时确保Core Graphics会将你在-drawInContext
中绘制的东西放入到纹理的位图数据中。




渲染总流程

iOS绘制与渲染--渲染流程_第5张图片
CPU与GPU协作
iOS绘制与渲染--渲染流程_第6张图片
同步时钟触发重绘
iOS绘制与渲染--渲染流程_第7张图片
掉帧卡顿问题

在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

渲染时机

上面已经提到过:Core Animation 在 RunLoop 中注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件 。当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。当Oberver监听的事件到来时,回调执行函数中会遍历所有待处理的UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                          [CALayer layoutSublayers];
                          [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                          [CALayer display];
                          [UIView drawRect];

渲染具体步骤

动画和屏幕上组合的图层实际上被一个单独的进程管理,即所谓的渲染服务。
当运行一段动画时,这个过程会被四个分离的阶段打破:

  1. 布局--准备视图的层级关系,设置图层属性
  2. 显示--图层的寄宿图片被绘制的阶段。涉及到-drawRect和-drawLayer:inContext:等方法
  3. 准备--准备发送动画数据给渲染服务的阶段。比如图片解码
  4. 提交--打包所有图层和动画属性,通过IPC发送到渲染服务

渲染服务拿到数据后,反序列化成一个叫做渲染树的图层树,使用这个树状结构,渲染服务队动画的每一帧做如下工作:

  1. 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化三角形)来执行渲染
  2. 在屏幕上渲染可见的三角形

所以一共六个阶段:最后两个阶段在动画过程中不停地重复,前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。剩下的在CoreAnimation内部处理。

CADisplayLink简介

当你设置一个NSTimer,他会被插入到当前任务列表中,然后直到指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。

用CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑,但即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会导致动画偶尔地丢帧。当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。

参考文章

iOS开发-视图渲染与性能优化
iOS 事件处理机制与图像渲染过程

你可能感兴趣的:(iOS绘制与渲染--渲染流程)