Appearance
ios-image-loading-sim
Swift Playground 版:单文件 Swift,macOS 直接
swift ImageLoadingSim.swift运行,纯数据结构模拟 iOS 图片解码与缓存策略。
1. 实验目标与工程要点
- 验证位图解码内存公式 宽 × 高 × 每像素字节数:一张 3.05MB 的 JPG 全量解码后占 46.5MB——内存只看像素面积,与文件体积无关。
- 验证解码期降采样(
CGImageSourceCreateThumbnailAtIndex的maxPixelSize语义):峰值 = 缩略图大小,对比「先全图解码再缩放」的高峰路径。 - 模拟 NSCache 双限 LRU 语义(
UIImage(named:)系统级内存缓存的核心):totalCostLimit超预算按最久未用逐出、命中零重解码、内存警告整体清空。 - 全部数值可断言:48771072 字节、388800 字节、逐出顺序
[icon, banner]。
2. 深入原理与生活浅显比喻
2.1 生活浅显比喻
JPG 像真空压缩过的羽绒服,解压后占多大取决于布料面积(像素数),不是压缩袋大小(文件体积)。降采样像按床的尺寸买被套:直接裁成需要的大小(解码期降采样),而不是先展开整床被子再折小(全图解码再缩放)——后者峰值还是整床被子的占地。NSCache 像有面积上限的照片墙:贴新照片超面积时,最久没人看的那张先被撤下。
2.2 底层原理分析
- 解码内存 = 解码后宽 × 高 × BPP(sRGB RGBA8888 为 4,等价 Android ARGB_8888);iOS 无 RGB_565 式开关,控内存主要靠降采样。
CGImageSourceCreateThumbnailAtIndex(maxPixelSize:)在解码阶段直接输出最长边 ≤ maxPixelSize 的等比缩略图;对照 AndroidinSampleSize(n² 级缩减)与 FluttercacheWidth/cacheHeight。- NSCache 是「LRU + cost/count 双限 + 自动响应内存警告」的系统缓存;
UIImage(named:)走它所以同名重复取图不重解码,UIImage(contentsOfFile:)无缓存。对照 Glide LruCache 与 Flutter ImageCache(1000 张 / ~100MB 双限)——同一套模型。
2.3 方案对比矩阵
| 方案 | 通俗比喻 | 底层原理 | 保证特性 | 吞吐性能 | 运行开销 | 适用场景 |
|---|---|---|---|---|---|---|
| 全量解码再缩放 | 先展开整床被子再折小 | 先按原始像素解码进内存,再二次缩放 | 实现简单 | 低 | 峰值 = 原图(本 demo 中 46.5MB) | 仅极小图可接受,大图必 OOM 风险 |
| 解码期降采样 | 按床的尺寸直接裁被套 | maxPixelSize / inSampleSize / cacheWidth 在解码阶段输出目标尺寸 | 峰值 = 缩略图 | 高(省 99%+) | 极低 | 列表缩略图、头像、按 View 尺寸展示大图 |
| NSCache 内存缓存 | 有面积上限的照片墙 | LRU + totalCostLimit/countLimit + 内存警告清空 | 命中零重解码、自动让路 | 高 | 极低 | 本地/网络图重复展示的第一级 |
3. 移动端实战场景与误配事故
3.1 实战场景
- 聊天列表头像:网络图下载后用
maxPixelSize ≈ 头像物理尺寸降采样再入 NSCache,千条消息内存恒定。 - 相册九宫格 → 大图查看器:列表用缩略图缓存,查看器才加载原图并配合
scaleAspectFit。
3.2 常见误配与事故注意事项
- 用
UIImage(data:)直接解 4032×3024 原图塞进 36pt 头像框 → 单张 46MB、九宫格即 OOM。 - 把 NSCache 当持久存储(存 token、DB 数据):它会在内存警告时整体清空,普通字典语义不成立。
- 自己用 Dictionary 手写图片缓存:既没有双限逐出也没有内存警告响应,泄漏风险高。
3.3 排障思路
图片内存问题先用「宽 × 高 × 4」心算单张理论值,对照 Instruments Allocations 看是否按理论值增长;命中差就查 key 是否稳定(同名 vs contentsOfFile);峰值高就确认是否走了解码期降采样而非显示缩放。
4. 实验源码与运行验证
关键源码
- ImageLoadingSim.swift —— 场景1 解码内存公式(48771072 字节);场景2
downsampled(maxPixelSize:)得到 360×270 缩略图(388800 字节);场景3SimNSCache双限 LRU(逐出[icon, banner]、HIT=1/MISS=3、内存警告清空)。
关键参数 / 开关
bytesPerPixel = 4:sRGB RGBA8888,等价 Android ARGB_8888。maxPixelSize:对应kCGImageSourceThumbnailMaxPixelSize,限制最长边等比降采样。totalCostLimit:NSCache 成本上限(本 demo 取 10MB);countLimit条目数上限兜底。removeAll():模拟didReceiveMemoryWarning时 NSCache 的自动清空行为。
运行方式
bash
cd labs/swift-playground/ios-image-loading-sim
swift ImageLoadingSim.swift # 直接运行演示
./verify.sh # 运行 + 断言校验预期控制台运行输出(节选)
text
[IMG] 全量解码后:4032x3024x4 = 48771072 字节(46.51MB)
[IMG] maxPixelSize=360 -> 360x270,解码内存 388800 字节(0.37MB)
[CACHE][named-cache] LRU 逐出 icon(最久未使用,成本已超限)
[CACHE][named-cache] get avatar -> HIT(复用已解码图像,不重解码)
PASS: 全部检查通过断言脚本说明
verify.sh 校验三点:① Swift 内置 5 项自检全部 PASS;② 关键数值 48771072 / 388800 出现;③ LRU 逐出顺序恰为 [icon, banner]。任一失败退出码为 1。
5. 对应知识库文档
- 理论主文档:iOS 图片加载(UIImage / UIKit / ImageIO)
- 公式与降采样总纲:图片加载方式总览与原理
复习检查题
为什么 demo 里 JPG 只有 3.05MB,解码后却是 46.5MB?
答:解码内存 = 像素面积 × BPP = 4032×3024×4 = 48771072 字节,与压缩文件体积无关;JPG 只是压缩格式,上屏前必须解压成位图。
「先全图解码再缩放」和「解码期降采样」的峰值差异有多大?
答:前者峰值仍是原图 46.5MB;后者在解码阶段直接输出 maxPixelSize 小图,峰值仅 0.37MB(本 demo 省 99%)。
NSCache 和普通字典的区别是什么?
答:NSCache 有 LRU + totalCostLimit/countLimit 双限逐出,且收到系统内存警告会自动清空腾内存;字典两者皆无,当缓存用会持续堆积直至 OOM。
UIImage(named:)和UIImage(contentsOfFile:)在缓存行为上的差异如何用本 demo 对应?答:named 走系统级 NSCache(demo 的 named-cache:命中 HIT 不重解码);contentsOfFile 等价于每次都 MISS 后重新「解码」,适合一次性临时文件。