UIView.cheetah是怎么实现的:关联对象与自定义属性动画扩展源码剖析
【免费下载链接】CheetahEasy animation library on iOS with Swift2项目地址: https://gitcode.com/gh_mirrors/chee/Cheetah
Cheetah 是一个基于 Swift 的 iOS 轻量级动画库,它通过一句view.cheetah.move(100, 0).run()就能驱动任意 UIView 属性做链式动画。今天我们就以UIView.cheetah这个入口为线索,剖析它背后的两个核心机制:Objective-C 关联对象(Associated Objects)与自定义属性动画系统,看懂它如何在不继承、不修改 UIView 源码的前提下"挂"上动画能力。
一、Cheetah 是什么:一句话带动画
🦁 Cheetah(猎豹)的核心理念写在官方说明里:它不依赖 Core Animation 的隐式动画,而是用一个简单的CADisplayLink 运行循环逐帧修改属性值,因此"任何属性"都可以被动画化——包括 NSLayoutConstraint 的 constant 这种系统动画根本碰不到的东西。
主要特性一览:
| 特性 | 说明 |
|---|---|
| 时长与延迟 | 每个属性可独立设置 duration / delay |
| 绝对/相对模式 | position绝对定位,move相对位移 |
| 并行/串行执行 | 同一组属性并行,wait()切组串行 |
| 缓动函数 | 内置 Sine、Quad、Cubic、Elastic、Bounce 等 30+ 曲线 |
| 弹簧动力学 | 基于 tension / friction 参数的 Spring 动画 |
源码入口是 Cheetah.podspec,库本体全部集中在Cheetah/目录下,总共不到 15 个文件,非常适合通读。
二、UIView.cheetah 扩展属性:一行魔法代码
UIView.cheetah的全部实现只有 20 行,位于 Cheetah/UIView+CheetahAnimation.swift:
var cheetahAssociationKey = "cheetahAssociation" public extension UIView { public var cheetah: Cheetah { get { if let instance = objc_getAssociatedObject(self, &cheetahAssociationKey) as? Cheetah { return instance } let instance = Cheetah(view: self) objc_setAssociatedObject(self, &cheetahAssociationKey, instance, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) return instance } } }这段代码做了一件很巧的事:
- 首次访问:字典里查不到 Cheetah 实例,就新建一个并通过
objc_setAssociatedObject绑到当前 UIView 上,记忆策略选的是.OBJC_ASSOCIATION_RETAIN_NONATOMIC(强引用、非原子); - 后续访问:
objc_getAssociatedObject直接取回同一个实例,保证链式调用状态不丢失; - 动画结束后:由管理器主动把关联对象置 nil,自动"卸载"自己(后面第五节细说)。
📌 这就是典型的关联对象扩展模式:Swift 的 extension 不能存储属性,于是借用 Objective-C Runtime 给对象"外挂"一个键值对,相当于给 UIView 动态加了一个cheetah实例变量。
三、为什么关联对象能行:Runtime 原理 30 秒理解
💡 每个 OC 对象(UIView 本质上是 OC 类)在 Runtime 中都维护着一张关联对象表。objc_setAssociatedObject实际上是往这张表里插入一条"键(任意指针)→ 值"的映射。
Cheetah 的选择很有讲究:
- 键是全局变量的地址:
&cheetahAssociationKey指向一个全局字符串变量的内存地址,进程内天然唯一,不会与其他库撞键; - 键定义在 extension 文件作用域而非 Cheetah 类内部,保证了键的生命周期与库一致;
- 弱引用 view + 强引用 Cheetah的组合(见 Cheetah/Cheetah.swift):
Cheetah内部持有weak var view: UIView?,即"view 强持 Cheetah,Cheetah 弱指回 view",不会形成循环引用,view 销毁后 Cheetah 自动释放,动画也会安全终止。
这套"关联对象 + 弱回指"的写法,是 iOS 上给系统类扩展状态的标准方案,你在很多成熟库(如日志、埋点框架)里都能看到它的身影。
四、Cheetah 核心类:链式 API 与属性分组
拿到cheetah属性后返回的是 Cheetah/Cheetah.swift 里的核心类,它把所有动画 API 设计成返回 self 的链式方法:
// move / position / rotate / scale / alpha / backgroundColor ... open func move(_ x: CGFloat, _ y: CGFloat) -> Cheetah open func rotation(_ angle: Double) -> Cheetah open func alpha(_ alpha: CGFloat) -> Cheetah open func constraint(_ constraint: NSLayoutConstraint!, constant: CGFloat) -> Cheetah内部数据模型是两层结构:
- CheetahProperty(属性):一次动画的最小单位,记录 from/to、duration、delay、easing 等,定义在 Cheetah/CheetahProperties.swift;
- CheetahGroup(组):一组并行执行的属性,组与组之间串行。
wait()方法(Cheetah/Cheetah.swift)就是"压入新组",从而实现move → wait() → rotate这种先后顺序。
几个值得注意的设计细节:
duration(_:)会同时写给最后一个属性和当前组(Cheetah/Cheetah.swift),所以先写.duration(0.5)再写多个属性,后续属性会"继承"这个时长;- 缓动函数全部以闭包形式提供:
public typealias Easing = (_ t:CGFloat, _ b:CGFloat, _ c:CGFloat) -> CGFloat(Cheetah/Easings.swift),30 多个内置曲线大多通过贝塞尔求解器Easings.cubicBezier生成(Cheetah/Easings.swift),你也能自己传任意闭包进来; - 弹簧动画由独立的 Cheetah/Spring.swift 实现,通过
spring(tension:friction:)挂载到属性上(Cheetah/Cheetah.swift)。
五、动画如何逐帧驱动:CADisplayLink 单例管理器
链式调用只是"配置",真正跑起来靠run()→ 单例 Cheetah/CheetahManager.swift 的帧循环:
run() → Manager.add(self) → 创建 CADisplayLink → 每帧回调 update()update(_:)每帧做的事(Cheetah/CheetahManager.swift):
- 用
displayLink.timestamp差值算出本帧dt; - 遍历所有注册中的 Cheetah,逐个调用
proceed(dt)推进时间轴; - 某个 Cheetah 完成后,把它的关联对象置为 nil(
objc_setAssociatedObject(view, &cheetahAssociationKey, nil, ...)),即自动从 view 上"卸载"自己,避免陈旧的动画状态残留; - 当注册表为空时
invalidate()掉 DisplayLink,零动画时零开销。
🎯 逐帧推进的核心数学很简单:每个CheetahProperty.proceed(dt)累加elapsed,算出进度current = (elapsed - delay) / duration,再经缓动函数插值后写回真实属性(Cheetah/CheetahProperties.swift)。这正是它能动画"任意属性"的原因——每帧直接赋值,不挑属性类型。
旋转、缩放这类变换类属性走的是另一条路:不直接改layer.transform,而是各自算出一个局部变换矩阵,最后由 Cheetah/CheetahGroup.swift 用CATransform3DConcat合并后一次性写回,因此同一组里 rotate + scale 可以完美叠加、互不覆盖。
六、自定义属性动画扩展:照着模板写三个方法
🛠️ Cheetah 最迷人的能力是"动画你自己的属性"。以透明度为例,Cheetah/CheetahViewProperties.swift 的模板极其清晰:
class CheetahViewAlphaProperty: CheetahCGFloatProperty { override func prepare() { super.prepare() from = view.alpha // 1. 记录起点 } override func update() { view.alpha = calculateCGFloat(from: from, to: toCalc) // 2. 插值并写回 } }继承合适的基类(CGFloat/CGPoint/CGSize/CGRect/UIColor,见 Cheetah/CheetahProperties.swift),实现prepare(抓 from 值)和update(写当前值)即可。更硬核的例子是约束动画:Cheetah/CheetahConstraintProperties.swift 每帧直接改写constraint.constant,这是 UIView.animate 根本做不到的。
内置属性类分布:
- 视图类:Cheetah/CheetahViewProperties.swift(size / frame / alpha / backgroundColor)
- 图层类:Cheetah/CheetahLayerProperties.swift(position / rotation / scale / 边框圆角)
- 约束类:Cheetah/CheetahConstraintProperties.swift
七、总结:一套可复用的 iOS 扩展设计范式
📖 把UIView.cheetah的实现拆开看,其实是一份非常精炼的设计范式清单:
- 关联对象解决"Swift 扩展无存储"问题,键用全局变量地址,动画结束自动卸载;
- 弱引用回指切断循环引用,让 view 的生命周期天然约束动画的生命周期;
- 链式 API + 组/属性双层模型,用
wait()一个方法同时获得并行与串行能力; - CADisplayLink 集中管理,单例管理器共享一个 DisplayLink,空载即停;
- prepare / calc / update 三段式属性模板,让"任意属性可动画"成为低成本的扩展点。
如果你正在做 iOS 动画、或者想给系统类外挂状态,这套不到千行的源码(主目录 Cheetah/)值得完整读一遍——它几乎浓缩了 Runtime 扩展与帧驱动动画的全部关键技巧。
【免费下载链接】CheetahEasy animation library on iOS with Swift2项目地址: https://gitcode.com/gh_mirrors/chee/Cheetah
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考