Skip to content

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 的等比缩略图;对照 Android inSampleSize(n² 级缩减)与 Flutter cacheWidth/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 字节);场景3 SimNSCache 双限 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. 对应知识库文档 ​

复习检查题 ​

  1. 为什么 demo 里 JPG 只有 3.05MB,解码后却是 46.5MB?

    答:解码内存 = 像素面积 × BPP = 4032×3024×4 = 48771072 字节,与压缩文件体积无关;JPG 只是压缩格式,上屏前必须解压成位图。

  2. 「先全图解码再缩放」和「解码期降采样」的峰值差异有多大?

    答:前者峰值仍是原图 46.5MB;后者在解码阶段直接输出 maxPixelSize 小图,峰值仅 0.37MB(本 demo 省 99%)。

  3. NSCache 和普通字典的区别是什么?

    答:NSCache 有 LRU + totalCostLimit/countLimit 双限逐出,且收到系统内存警告会自动清空腾内存;字典两者皆无,当缓存用会持续堆积直至 OOM。

  4. UIImage(named:) 和 UIImage(contentsOfFile:) 在缓存行为上的差异如何用本 demo 对应?

    答:named 走系统级 NSCache(demo 的 named-cache:命中 HIT 不重解码);contentsOfFile 等价于每次都 MISS 后重新「解码」,适合一次性临时文件。

站点构建时间:2026/8/24 23:43:17