2、Core Animation架构解析:CALayer的绘制原理、渲染树与模型树的关系、隐式动画机制
Core Animation,说白了就是iOS动画的基石。很多开发者用了好几年,却只停留在「调个API让View动起来」的层面。我个人觉得,不理解Core Animation的架构,你写出来的动画永远都是「能用,但不够好」。
这一章,我们深入底层。聊聊CALayer到底怎么画出来的,模型树和渲染树是什么关系,以及那个让人又爱又恨的隐式动画。
2.1 CALayer的绘制原理
先问一个问题:UIView和CALayer,到底是什么关系?
UIView是显示内容的载体,而CALayer才是真正干活的。每个UIView背后,都有一个CALayer在支撑。你看到的圆角、阴影、边框,其实都是Layer的功劳。
绘制流程大概是这样的:
- 你调用
setNeedsDisplay,标记Layer需要重绘 - RunLoop在下一次刷新时,调用
display方法 - 如果实现了
drawLayer:inContext:代理,系统会创建backing store - 你的绘制代码在CPU上执行,生成位图
- 位图通过IPC传给渲染服务进程
- 渲染服务把位图上传到GPU纹理
- GPU合成并显示到屏幕
关键点:绘制是在CPU上完成的,合成是在GPU上完成的。这两个阶段,哪个慢了都会卡。
我在项目中遇到过一个问题:一个列表页,每个cell都有复杂的圆角+阴影。滑动起来掉帧严重。后来发现,每次cell复用都会触发 drawRect:,CPU瞬间飙高。解决方案是用 shouldRasterize 把绘制结果缓存成位图。嗯,这里要注意,光栅化只适合内容不变的场景。
2.2 模型树与渲染树的关系
Core Animation内部维护了两棵树:模型树(Model Tree)和渲染树(Presentation Tree)。
模型树,存的是你设置的属性值。比如 layer.frame = CGRectMake(0, 0, 100, 100),模型树里就记着这个值。
渲染树,存的是当前屏幕上显示的值。动画进行到一半时,模型树的值还是终点值,但渲染树的值是中间值。
为什么要搞两棵树?
你想想看,动画是一帧一帧变化的。如果只有一棵树,动画过程中你读取 layer.frame,得到的是起点还是终点?这会造成混乱。两棵树分离后,模型树始终保存「最终状态」,渲染树保存「当前显示状态」。各司其职,互不干扰。
| 属性 | 模型树 | 渲染树 |
|---|---|---|
| 存储内容 | 你设置的最终值 | 当前屏幕显示值 |
| 读取方式 | layer.frame |
layer.presentation()?.frame |
| 动画过程中 | 不变 | 逐帧变化 |
| 用途 | 布局计算、事件处理 | 获取当前动画位置 |
实用技巧:如果你想在动画过程中获取Layer的实时位置(比如做碰撞检测),一定要用 presentation()。我曾经有个需求,动画中的小球需要和静态障碍物做碰撞检测。一开始我读的是 layer.frame,结果永远对不上。换成 presentation()?.frame 后,问题解决。
2.3 隐式动画机制
隐式动画,是Core Animation最神奇的特性之一。你只是改了个属性,它就自己动起来了。
// 创建一个Layer
let layer = CALayer()
layer.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
layer.backgroundColor = UIColor.red.cgColor
view.layer.addSublayer(layer)
// 改变颜色——它会自动动画
CATransaction.begin()
layer.backgroundColor = UIColor.blue.cgColor
CATransaction.commit()
为什么会这样?
因为CALayer内部有一个 actions 字典。当你修改某个可动画属性时,Layer会去这个字典里找对应的动画对象。如果找到了,就自动创建一个CABasicAnimation并执行。
哪些属性支持隐式动画?
position、bounds、framebackgroundColor、opacitycornerRadius、borderWidthtransform、shadowOffset等
避坑指南:UIView的layer默认禁用了隐式动画。因为UIView自己管理动画,不希望Layer「自作主张」。但如果你直接操作CALayer,隐式动画是默认开启的。我曾经在自定义转场动画时,直接修改了某个子Layer的position,结果出现了意料之外的动画。排查了半天才发现是隐式动画在捣鬼。
禁用隐式动画的方法也很简单:
CATransaction.begin()
CATransaction.setDisableActions(true)
layer.position = CGPoint(x: 200, y: 200)
CATransaction.commit()
或者更彻底一点,直接设置 actions 字典:
layer.actions = ["position": NSNull()]
我个人习惯用 CATransaction.setDisableActions(true),因为更直观。但如果你需要精细控制每个属性的动画行为,用 actions 字典会更灵活。
2.4 小结
这一章我们聊了三件事:
- CALayer的绘制流程——CPU画,GPU合成
- 模型树和渲染树——一个存终点,一个存当前
- 隐式动画——改属性就自动动,但要注意坑
理解这些底层机制,你写动画的时候就不会「知其然不知其所以然」了。下一章,我们会深入CABasicAnimation和CAKeyframeAnimation,看看怎么用代码精确控制动画的每一帧。