前四篇分别讲了架构、存储、读写流程和接入方式。最后这一篇只回答一个问题:
如果今天把 Fast Image Cache 当成生产依赖或改造蓝本,哪些源码边界必须提前处理?
读完这一篇,你会知道:
哪些问题可能导致请求挂住、静默失败或崩溃。 哪些问题是年代背景导致的现代化适配成本。 如果重写类似设计,应该在哪些接口和异常路径上加强防御。
先给结论
Fast Image Cache 作为源码学习材料非常经典,但不适合不加审查就直接放进现代新项目。
更稳妥的判断是:
学习它的缓存模型:值得。 复用它的整体架构:要结合业务规模评估。 原样作为现代生产依赖:风险偏高。 参考它重写一个更现代的小图缓存:更合理。
下面按风险类型拆开看。
API 语义风险
P1:nullable completion block 处理不完整
触发条件:
头文件里setImage、retrieveImage、asynchronouslyRetrieveImage 的 completion block 都是 nullable。
但实现里,setImage:forEntity:withFormatName:completionBlock: 只有在 completion block 非空时才构造completionBlocksDictionary。后续_processImage 又会通过completionBlocksDictionary.allKeys 决定要处理哪些 format。
类似地,retrieveImage miss 时,如果 completion block 为 nil,_FICAddCompletionBlockForEntity 不会为 format 放入 block 数组;source image 加载回来后,formatsToProcessForCompletionBlocks: 只看 completion blocks 的 key,可能不会写入请求的 format。
可能后果:
API 看起来允许“不关心回调,只想预热缓存”,但源码实现并没有把这个场景完整走通。轻则请求没有产生预期写入,重则在某些 Foundation 行为下触发 nil 参数异常。
改造建议:
把“请求的 formatName”作为独立输入传给处理流程,不要从 completion block 字典反推要处理哪些 format。completion block 是否存在,只应该影响是否回调,不应该影响是否处理缓存。
P1:delegate 不回调会让请求挂住
触发条件:
缓存 miss 后,如果 entity 没有通过fic_imageForFormat: 直接给出 image,就会走 delegate:
如果 delegate 没实现这个方法,或者实现了但失败分支没有调用 completion block,_requests 中对应 source URL 的请求就不会被清理。
可能后果:
当前 completion block 不会被调用 后续相同 source URL 的请求会被认为“已经在加载中” completion blocks 和 entity 可能被 _requests持续持有
改造建议:
请求对象应该有明确的完成态:success、failure、cancelled、timeout。delegate 不回调时,缓存层也应该能超时或失败收敛,而不是把正确性完全交给业务方。
P2:未知 formatName 可能静默失败
触发条件:
调用方传入一个没有注册过的formatName,_imageTables[formatName] 会是 nil。
Objective-C 给 nil 发消息不会崩,所以前半段看起来还能继续走。但 miss 后 source image 加载回来,_processImage 找不到 image table,就不会写入,也不会调用 completion block。
可能后果:
调用方只看到“图片没有回来”,但很难从 API 层知道是 formatName 未注册。setImage 对未知 format 会记录错误;retrieveImage 这条路径的失败反馈弱得多。
改造建议:
在 retrieve 一开始检查 image table 是否存在;不存在就立刻回调 nil,并通过统一错误通道报告 unknown format。
数据一致性风险
P1:metadata 损坏后的恢复逻辑不够硬
触发条件:
metadata 写入使用atomically:NO,应用崩溃、系统杀进程或磁盘异常时,理论上可能留下半截 JSON。
读取时_loadMetadata 会尝试 JSON,失败后再尝试旧版 plist。这个兼容思路没问题,但后续处理不够防御式:
如果 metadata 无法解析,代码仍然会继续尝试从 nil 字典里取 index map。 如果 format 字典不匹配,源码会删除 table 文件和 metadata 文件,并把 metadataDictionary置 nil,但后面仍继续执行 setDictionary 之类的加载逻辑。
可能后果:
缓存目录本来就是可能被系统清理、被异常中断写入的地方。恢复路径不够硬,会让“缓存损坏”从一个可重建问题,变成启动时异常或状态不一致问题。
改造建议:
发现 metadata 无效或 format 变化后,应立刻清空内存状态并 return。更现代的实现还可以使用 atomic write、临时文件替换和显式 schema version。
P2:format 变化后的清理路径应该更早收敛
触发条件:
format 字典不匹配时,源码会删除 table 文件和 metadata 文件。
这个策略本身是正确的,但删除后仍继续向下走加载逻辑,防御性不足。
可能后果:
理想路径下不一定出问题,但异常状态下容易出现“磁盘已经清了,内存还按旧 metadata 继续初始化”的认知不一致。
改造建议:
format mismatch 应该被当成明确的重建分支:清文件、清内存、创建空状态,然后退出当前加载流程。
mmap 生命周期风险
P0:reset 可能和仍在显示的 mapped image 打架
触发条件:
普通删除 entry 只是移除 metadata,不会立刻截断文件,所以已经返回给 UI 的 image 还能继续使用原来的 mapped bytes。
但reset 会清空 chunk 字典,并通过ftruncate 把表文件长度设为 0。
如果这时还有UIImage /CGImage 持有旧的FICImageTableEntry,它背后的 mapping 可能还活着,但文件已经被截断。
可能后果:
后续渲染访问这些页时,存在触发底层访问错误的风险。这类问题通常不容易复现,但一旦发生就是比较严重的稳定性问题。
改造建议:
reset 不适合在界面仍可能显示缓存图片时随便调用。更安全的策略是:
在没有可见 image 使用该 table 时 reset 或者改造成“换新表文件 + 延迟清理旧文件”的模型 或者给 table generation 加引用计数,让旧 generation 的 mapping 自然释放后再删除文件
P1:partial chunk offset 计算有疑点
触发条件:
FICImageTable 在_chunkAtIndex: 里计算 chunk offset 时用的是:
如果最后一个 chunk 是 partial chunk,会把 length 缩短。
但FICImageTableChunk 初始化时又自己算了一次:
正常情况下,Fast Image Cache 扩容会让文件长度保持 chunk 对齐,所以这个问题不容易暴露。
可能后果:
如果遇到历史版本文件、外部截断、异常损坏,最后一个 chunk 的length 小于完整 chunk 长度,index * length 就不是正确 offset。
改造建议:
由FICImageTable 把已经算好的chunkOffset 传给FICImageTableChunk,不要让 chunk 用可能变化的 length 反推 offset。
输入校验风险
P1:UUID 字符串必须合法,但源码没有兜底
触发条件:
FICEntity 要返回fic_UUID 和fic_sourceImageUUID。源码后续会调用:
这个函数内部使用CFUUIDCreateFromString。如果业务方返回的不是合法 UUID 字符串,比如直接返回"user_123",CFUUIDCreateFromString 可能失败,而函数没有给CFUUIDBytes 做安全默认值。
可能后果:
缓存校验异常,严重时可能写入未初始化的 UUID bytes。
改造建议:
注册 entity 或读取 UUID 时就做强校验。失败时立刻拒绝请求并报告错误。业务接入层应该提供稳定 ID 到 UUID string 的转换工具,避免每个模型自己拼。
P1:CGBitmapContextCreate 失败后没有保护
触发条件:
写入 entry 时,源码会创建 bitmap context:
但它没有检查返回值是否为 NULL,就把 context 传给业务 drawing block。
可能后果:
如果 image format 配置非法,比如尺寸为 0、像素格式组合不被支持、row bytes 不合法,或者底层资源异常导致 context 创建失败,drawing block 里继续使用这个 context 就可能崩。
改造建议:
format 注册阶段就做强校验;写入阶段对CGBitmapContextCreate 返回值做 NULL 检查,失败后标记请求失败并清理 entry 状态。
P2:namespace 和 format name 缺少路径安全校验
触发条件:
format.name 会直接参与文件名生成,namespace 会直接参与目录拼接。
官方建议用反向 DNS 命名,这是合理约束;但源码没有阻止这些字符串里出现/、.. 或其他不适合作为路径组件的字符。
可能后果:
如果这些值完全来自业务常量,风险可控;如果来自配置下发或外部输入,就可能带来路径混乱甚至越界写入风险。
改造建议:
把 namespace 和 format name 当作路径组件处理:限制字符集、拒绝路径分隔符、拒绝..、必要时做 percent escaping 或 hash 映射。
改造建议
1. 替换过时原子 API
源码里使用了OSAtomicIncrement32 这类早期 API。
今天更合适的做法是使用 C11 atomic、stdatomic.h,或者把计数纳入已有锁保护范围。
2. 重新设计多实例队列
所有 cache 实例共享一条串行队列,实现简单,但现代复杂业务里可能成为吞吐瓶颈。
更合理的设计是:
每个 namespace 或 table 有独立执行队列 全局只保留轻量调度 同一 table 内部串行,跨 table 允许并发 drawing block 慢时不阻塞无关 namespace
3. 明确 UIKit 线程边界
drawing block 在后台串行队列执行。
这意味着业务方不能在 block 里随便操作 UIKit view 层级,也不能依赖主线程状态。现代实现应该在 API 文档和类型命名上明确这一点,必要时把绘图输入限制成纯数据。
4. format 注册后不可变
FICImageFormat 对外是可变对象,setFormats 后如果调用方继续修改 format,cache 内部字典和 table copy 可能出现认知不一致。
更稳的方式是让 format 成为不可变配置对象。注册后 cache 内部只持有 immutable snapshot,后续修改必须重新注册并触发表重建。
5. 错误回调要成为一等语义
原实现更偏 Objective-C 早期风格:很多失败路径只是记录错误,或者依赖 nil message 继续走。
现代实现应该让错误回调成为一等语义:
unknown format invalid UUID invalid image format source image load failed bitmap context create failed metadata recovery failed reset delayed because mapped image still in use
有明确错误,调用方才知道是接入问题、数据问题,还是缓存内部恢复问题。
值得保留的设计
虽然这一篇在讲风险,但 Fast Image Cache 仍然有几条设计值得保留。
第一,缓存最终展示形态。
很多图片性能问题不是下载慢,而是每次显示前都要重新处理。如果 UI 最终需要圆角头像、固定尺寸封面、小尺寸灰度图,缓存层就应该考虑直接缓存这些结果。
第二,缓存键表达业务失效关系。
fic_UUID 和fic_sourceImageUUID 的拆分非常值得借鉴。实体身份和资源版本分开,缓存失效会清楚很多。
第三,高性能缓存要关心消费者。
Fast Image Cache 不只关心“我能不能把 bytes 存起来”,还关心 Core Animation 拿到这些 bytes 时会发生什么。
只在缓存内部自洽,不一定真的快。缓存的最终消费者如果是渲染系统,就要关心像素格式、对齐和解码时机。
这一篇解决了什么
这一篇把 Fast Image Cache 的风险按类型重新整理了:
API 语义风险:nullable callback、delegate 不回调、unknown format 数据一致性风险:metadata 损坏、format mismatch 恢复路径 mmap 生命周期风险:reset 和 partial chunk offset 输入校验风险:UUID、bitmap context、路径组件 现代化改造:atomic、队列、UIKit 边界、immutable format、错误语义
如果今天要借鉴这套设计,我建议保留思想,重写实现:
缓存最终展示形态,缓存键表达业务失效关系,让缓存输出贴近渲染系统真正需要的内存格式;但所有异常路径、生命周期和错误语义都按现代生产要求重新设计。
参考资料
Fast Image Cache 仓库:https://github.com/path/FastImageCache 本文源码基线提交:https://github.com/path/FastImageCache/commit/2615d275abe6195f4a90a7b46593768b74b3b273 README 原理说明:https://github.com/path/FastImageCache/blob/master/README.md FICImageCache.m:https://github.com/path/FastImageCache/blob/2615d275abe6195f4a90a7b46593768b74b3b273/FastImageCache/FastImageCache/FastImageCache/FICImageCache.mFICImageTable.m:https://github.com/path/FastImageCache/blob/2615d275abe6195f4a90a7b46593768b74b3b273/FastImageCache/FastImageCache/FastImageCache/FICImageTable.mFICImageTableChunk.m:https://github.com/path/FastImageCache/blob/2615d275abe6195f4a90a7b46593768b74b3b273/FastImageCache/FastImageCache/FastImageCache/FICImageTableChunk.mFICImageTableEntry.m:https://github.com/path/FastImageCache/blob/2615d275abe6195f4a90a7b46593768b74b3b273/FastImageCache/FastImageCache/FastImageCache/FICImageTableEntry.mFICImageFormat.m:https://github.com/path/FastImageCache/blob/2615d275abe6195f4a90a7b46593768b74b3b273/FastImageCache/FastImageCache/FastImageCache/FICImageFormat.mFICUtilities.m:https://github.com/path/FastImageCache/blob/2615d275abe6195f4a90a7b46593768b74b3b273/FastImageCache/FastImageCache/FastImageCache/FICUtilities.m
夜雨聆风