- 图形学
- 移动开发
【免费下载链接】lottie-ios
An iOS library to natively render After Effects vector animations
本指南聚焦于 lottie-ios 仓库中内嵌的 LRUCache 缓存库:它承担着动画模型、图片资源与 dotLottie 文件的缓存职责,其源码位于 Sources/Private/EmbeddedLibraries/LRUCache/LRUCache.swift,配套说明文档为 Sources/Private/EmbeddedLibraries/LRUCache/README.md。读完本文,你将理解 Lottie 为何要把第三方库源码直接编译进自身二进制、LRUCache 的哈希表 + 双向链表实现如何保证 O(1) 的读写与淘汰,以及它相比 NSCache 的关键优势,并掌握该目录的版本更新操作流程。
一、为什么 Lottie 要"内嵌"一个第三方缓存库
Sources/Private/EmbeddedLibraries/LRUCache/README.md 开篇即说明了这一目录的来历:这里存放的是 LRUCache 库的源码,对应其 1.0.4 版本。
Lottie 通过多种包管理器对外分发——Swift Package Manager(SPM)、CocoaPods、Carthage 以及 NPM,而每种包管理器都有自己的打包与编译约束:
- SPM 依赖声明基于
Package.swift中的 target 与 dependency 解析; - CocoaPods 通过 podspec 声明依赖并生成闭源静态库/框架;
- Carthage 要求预先编译好的 framework;
- NPM 则是面向 React Native / Web 侧的分发通道。
由于这些包管理器的限制,Lottie 无法以"依赖一个独立 LRUCache 模块"的方式引用它。如果把它声明为外部依赖,不同包管理器下的依赖解析与编译产物会不一致,甚至可能引入版本冲突。因此,Lottie 的工程策略是:把 LRUCache 的源码直接包含在 Lottie 库内,与整个 Lottie 编译为单一单元。这也解释了为何它位于Sources/Private/(私有实现区)而不是Sources/Public/(公共 API 区)——内嵌库对 Lottie 的使用者完全不可见,不构成公共 API 面。
这一策略并非 LRUCache 独有,Sources/Private/EmbeddedLibraries/README.md 显示同目录下还内嵌了 ZipFoundation 与 EpoxyCore,三者的维护策略一致:拉取上游源码、更新版本说明、将public符号改为internal。
二、LRUCache 在 Lottie 中的三个真实使用场景
内嵌的 LRUCache 泛型类在 Lottie 中被实例化为三处核心缓存,从源码引用可以逐一印证:
1. 动画模型缓存:DefaultAnimationCache
Sources/Public/AnimationCache/DefaultAnimationCache.swift 是 Lottie 默认的动画缓存实现:
public final class DefaultAnimationCache: AnimationCacheProvider { private static let defaultCacheCountLimit = 100 private let cache = LRUCache<String, LottieAnimation>() // ... public func animation(forKey key: String) -> LottieAnimation? { cache.value(forKey: key) } public func setAnimation(_ animation: LottieAnimation, forKey key: String) { cache.setValue(animation, forKey: key) } }它在初始化时设置cache.countLimit = 100,即最多缓存 100 个解析完成的LottieAnimation模型;超过上限后,最久未被访问的动画会被自动淘汰。DefaultAnimationCache.sharedCache是全局共享实例,被 LottieAnimationCache.shared 默认引用,所有LottieAnimationView的加载入口(LottieAnimationViewInitializers.swift 与 LottieAnimationHelpers.swift)都默认使用它。
2. 图片资源缓存:CachedImageProvider
Sources/Private/MainThread/LayerContainers/Utility/CachedImageProvider.swift 将 LRUCache 用作动画内嵌图片的二级缓存:
private var imageCache = LRUCache<String, CGImage>()它以资产 ID(asset.id)为键缓存解码后的CGImage。动画中多个图层引用同一张图片时,只需解码一次;该缓存随动画重置而清空。
3. dotLottie 文件缓存:DotLottieCache
Sources/Public/DotLottie/Cache/DotLottieCache.swift 同样以countLimit = 100配置 LRUCache 缓存DotLottieFile,并通过cacheSize属性对外暴露容量调整能力。
为何弃用 NSCache
三处缓存的源码注释都给出了同一结论(见 DefaultAnimationCache.swift):
我们使用 LRUCache 库而非 NSCache,因为 NSCache 会在 App 进入后台时清空所有缓存值,而 LRUCache 只在收到内存警告通知时才清空。
NSCache 的这一行为会导致频繁后台/前台切换时,已解析的动画模型反复失效、反复重建,降低用户体验;而 LRUCache 仅响应内存压力,配合 LRU 淘汰策略,让热数据在常规场景下得以保留。
另外,历史遗留的LRUAnimationCache类型名已废弃,Sources/Public/AnimationCache/LRUAnimationCache.swift 中它被定义为DefaultAnimationCache的类型别名,并注明新实现"线程安全且自动响应内存压力"。
三、源码级原理:哈希表 + 双向链表的 LRU 实现
内嵌的 Sources/Private/EmbeddedLibraries/LRUCache/LRUCache.swift 是 Nick Lockwood 所写 LRUCache 1.0.2 版本代码(目录 README 标注对应 1.0.4 发布版),核心数据结构简洁而经典:
final class LRUCache<Key: Hashable, Value> { private var values = [Key: Container]() private unowned(unsafe) var head: Container? private unowned(unsafe) var tail: Container? private let lock = NSLock() private(set) var totalCost = 0 var totalCostLimit: Int { didSet { clean() } } var countLimit: Int { didSet { clean() } } }- 哈希字典
values: [Key: Container]提供 O(1) 的按键查找; - 双向链表(
head/tail+ 每个Container的prev/next指针)维护访问顺序:链表头部是最久未使用(LRU)的条目,尾部是最近使用的条目; Container是 fileprivate 内部节点,持有value、cost、key及前后指针。
读操作:value(forKey:)
value(forKey:)命中时执行"摘除 + 追加到尾部",把刚访问的条目移到链表尾部,从而更新其"新鲜度":
func value(forKey key: Key) -> Value? { lock.lock() defer { lock.unlock() } if let container = values[key] { remove(container) append(container) return container.value } return nil }remove与append是私有辅助方法(注释要求"必须在锁内调用"),前者在 O(1) 时间内把节点从链表中摘除,后者把节点追加为新的 tail。由此,每次读/写都能保持 LRU 序。
写操作:setValue(_:forKey:cost:)
func setValue(_ value: Value?, forKey key: Key, cost: Int = 0) { guard let value else { removeValue(forKey: key) return } // 命中则更新值与 cost,摘除后重新 append // 未命中则创建 Container 并 append totalCost += cost lock.unlock() clean() }值得注意的两个细节:
- 传
nil即删除:setValue(nil, forKey:)等价于removeValue(forKey:),这在语义上很顺手; - cost 可选参数:每条目可附带一个成本值,累积为
totalCost,供totalCostLimit做"总成本"维度的淘汰判断。
淘汰机制:clean()
每次setValue之后、以及totalCostLimit/countLimit被修改时,都会触发clean():
private func clean() { lock.lock() defer { lock.unlock() } while totalCost > totalCostLimit || count > countLimit, let container = head { remove(container) values.removeValue(forKey: container.key) totalCost -= container.cost } }只要总成本或总数量超过任一上限,就持续从链表头部(最久未使用)淘汰,直到满足约束。这就是"最近最少使用"语义的落地。
线程安全与内存压力响应
- 所有可变操作(
value(forKey:)、setValue、removeValue、removeAllValues、allValues、clean)都通过NSLock加锁,因此DefaultAnimationCache等上层缓存可以被多线程并发访问; - 初始化时,LRUCache 会自动注册内存警告通知:在 iOS 上监听
UIApplication.didReceiveMemoryWarningNotification,在非 UIKit 平台(macOS / Linux / tvOS 等)则使用自定义通知名LRUCacheMemoryWarningNotification。收到通知后调用removeAllValues()清空缓存,这正是注释中"只响应内存压力而非后台切换"的实现基础:#if canImport(UIKit) let LRUCacheMemoryWarningNotification: NSNotification.Name = UIApplication.didReceiveMemoryWarningNotification #else let LRUCacheMemoryWarningNotification = NSNotification.Name("LRUCacheMemoryWarningNotification") #endif deinit中会移除观察者,避免泄漏。
两种淘汰维度
LRUCache 同时支持两种限制,可单独或组合使用(默认均为.max,即不限制):
| 参数 | 含义 | 默认值 | 触发淘汰条件 |
|---|---|---|---|
countLimit | 允许缓存的最大条目数 | .max | 条目数超过上限时淘汰最久未使用的条目 |
totalCostLimit | 允许缓存的最大总成本 | .max | 每条目可设cost,总成本超过上限时淘汰最久未使用条目 |
notificationCenter | 内存警告通知中心 | .default | 收到内存警告时清空全部缓存 |
Lottie 的三处缓存(动画、图片、dotLottie)均使用countLimit维度,其中动画与 dotLottie 缓存默认上限为 100。
四、如何将 LRUCache 更新到新版本(官方操作流程)
关联文档 Sources/Private/EmbeddedLibraries/LRUCache/README.md 明确给出了该目录的版本更新流程。如果你(或 Lottie 维护者)需要将内嵌的 LRUCache 升级到更新的上游版本,请严格按以下步骤操作:
- 下载最新发布版并替换源码:获取 LRUCache 项目的最新 release 源码,将其中的源码文件替换到
Sources/Private/EmbeddedLibraries/LRUCache/目录,覆盖旧的LRUCache.swift。 - 更新版本说明:修改 Sources/Private/EmbeddedLibraries/LRUCache/README.md 顶部的来源 URL,将其中的 release 标签更新为当前正在使用的版本,确保任何查看该目录的人都能明确知道内嵌的是哪个版本。
- 将
public符号改为internal:把新代码中所有public声明的符号改为internal(在 Swift 中直接删除public修饰符即为 internal),以防止 Lottie 对外暴露任何来自 LRUCache 的 API。
对照当前内嵌源码,可以确认这一约束已落实:Sources/Private/EmbeddedLibraries/LRUCache/LRUCache.swift 中的LRUCache类声明为final class(无访问修饰符,即 internal),Container为fileprivate final class,LRUCacheMemoryWarningNotification通知名为 internallet常量——三者均未泄漏到 Lottie 的公共接口中。
同样的更新纪律也适用于同目录的其他内嵌库,可参考 Sources/Private/EmbeddedLibraries/README.md:新增依赖时需要创建子目录、加入列表、编写同格式 README、在 Package.swift 的exclude:中排除该 README,并检查隐私清单的合并。
五、总结
从这份只有十几行的 README 出发,可以完整还原 Lottie 的嵌入式依赖治理思路:
- 为什么要内嵌:SPM / CocoaPods / Carthage / NPM 多分发渠道的打包约束,决定了无法把 LRUCache 作为独立模块依赖,只能源码级合入并统一编译;
- 内嵌后如何隔离:通过
internal化所有符号,让第三方实现停留在私有实现层(Sources/Private/EmbeddedLibraries/),不污染公共 API; - 内嵌价值何在:LRUCache 的哈希表 + 双向链表实现提供了 O(1) 读写与 LRU 淘汰,配合 NSLock 保证线程安全,并仅在内存警告时清空缓存——这正是 Lottie 的动画、图片与 dotLottie 三处缓存共同选择它的原因;
- 如何持续维护:遵循 README 中的三步更新流程(替换源码 → 更新版本说明 → 符号 internal 化),即可平滑升级内嵌版本。
如果你想深入验证 LRUCache 的行为,可以查看动画缓存入口 DefaultAnimationCache.swift、图片缓存包装 CachedImageProvider.swift、dotLottie 缓存 DotLottieCache.swift,以及协议定义 AnimationCacheProvider.swift,它们共同构成了 Lottie 高效的缓存链路。
- 图形学
- 移动开发
【免费下载链接】lottie-ios
An iOS library to natively render After Effects vector animations
相关推荐
uni-app Hello UTS 中的 Lottie iOS 内嵌 LRUCache:嵌入式依赖策略与 LRU 缓存实现解析
uni app Hello UTS 中的 Lottie iOS 内嵌 LRUCache:嵌入式依赖策略与 LRU 缓存实现解析 本篇技术指南以 LRUCache
示例工程前端移动开发跨平台lottie-ios缓存策略:多级缓存系统设计与实现
lottie ios缓存策略:多级缓存系统设计与实现 引言:动画加载的性能挑战 在移动应用开发中,动画效果已成为提升用户体验的关键要素。然而,复杂的Lottie
图形学移动开发Flipper Zero firmware内存管理:嵌入式设备内存优化策略
Flipper Zero firmware内存管理:嵌入式设备内存优化策略 引言:嵌入式设备内存管理的挑战 在资源受限的嵌入式设备开发中,内存管理是决定系统稳定
嵌入式物联网固件硬件开发渗透测试
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考