视图渲染框架
UIKit是常用的框架,显示、动画都通过CoreAnimation。CoreAnimation是核心动画,依赖于OpenGL ES做GPU渲染,CoreGraphics做CPU渲染;最底层的GraphicsHardWare是图形硬件。
下图是另外一种表现的形式。在屏幕上显示视图,需要CPU和GPU一起协作。一部数据通过CoreGraphics、CoreImage由CPU预处理。最终通过OpenGL ES将数据传送到 GPU,最终显示到屏幕。
CoreImage支持CPU、GPU两种处理模式。
显示逻辑
1、CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;
2、RenderServer解析提交的子树状态,生成绘制指令;
3、GPU执行绘制指令;
4、显示渲染后的数据;
提交流程(以动画为例)
第2步为prepare to commit animation (layoutSubviews,drawRect:);
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
中绘制的东西放入到纹理的位图数据中。
渲染总流程
在 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];
渲染具体步骤
动画和屏幕上组合的图层实际上被一个单独的进程管理,即所谓的渲染服务。
当运行一段动画时,这个过程会被四个分离的阶段打破:
- 布局--准备视图的层级关系,设置图层属性
- 显示--图层的寄宿图片被绘制的阶段。涉及到-drawRect和-drawLayer:inContext:等方法
- 准备--准备发送动画数据给渲染服务的阶段。比如图片解码
- 提交--打包所有图层和动画属性,通过IPC发送到渲染服务
渲染服务拿到数据后,反序列化成一个叫做渲染树的图层树,使用这个树状结构,渲染服务队动画的每一帧做如下工作:
- 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化三角形)来执行渲染
- 在屏幕上渲染可见的三角形
所以一共六个阶段:最后两个阶段在动画过程中不停地重复,前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。剩下的在CoreAnimation内部处理。
CADisplayLink简介
当你设置一个NSTimer,他会被插入到当前任务列表中,然后直到指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。
用CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑,但即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会导致动画偶尔地丢帧。当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。