Skip to content

iOS 图片加载(UIImage / UIKit / ImageIO) ​

回到总览:图片加载 / Image Loading引擎原理对照:02-图片加载方式总览与原理(Android Glide / Coil / Fresco 内部机制) 横向对比:03-图片加载对比(六类概念 / 考点 / 优势 / 性能) 相关模块:12/01 资源放置(iOS @2x/@3x 与大图放置策略)· 07/08 Bitmap 内存(内存公式 / 降采样对照)

一句话定义 ​

iOS 没有 Android 的「密度桶 + 解码缩放」,也没有 Flutter 的 asset 变体目录;它用 @2x/@3x 文件名后缀做离散 scale 精确匹配(UIImage(named:) 自动选档),用 ImageIO 的 CGImageSourceCreateThumbnailAtIndex 做解码期降采样,用 UIImageView.contentMode 做显示适配。官方不提供磁盘缓存,网络图靠 URLCache(HTTP 层)或第三方库(SDWebImage / Kingfisher)补内存 + 磁盘两级。

代码索引 ​

主题Lab 说明源码
iOS 解码内存公式 / 解码期降采样 / NSCache 双限 LRUios-image-loading-sim(Swift Playground 版,纯数据结构模拟,全部数值可断言)ImageLoadingSim.swift

1. 实现原理 ​

1.1 资源与 scale:@2x/@3x 离散匹配 ​

  • 设备 scale 只有 1x / 2x / 3x 三档(离散,详见 12/01 §4-5);UIImage(named: "icon") 会按当前设备 scale 自动找 icon.png / icon@2x.png / icon@3x.png,1:1 精确命中、零重采样——这就是 iOS 不需要密度桶的原因。
  • Asset Catalog 里把图片集设为 Single Scale(单源) =不按 @2x/@3x 分档,只有一份原始分辨率——正是大背景图 / 运营图的标准放法(对应 12/01 的 iOS 大图策略与 contentMode = .scaleAspectFill 自适应)。

1.2 加载链路:UIImage(named:) 有系统级内存缓存 ​

text
UIImage(named:) ──► UIKit 系统级内存缓存(NSCache,按名字)
                    ├─ 命中 → 直接复用已解码图像,不重复解码
                    └─ 未命中 → 从 Asset Catalog / Bundle 读字节 → 解码 → 入缓存
UIImage(contentsOfFile:) ──► 无缓存,每次都重新解码(适合临时文件)
  • 本地资源在 App 包内,UIImage(named:) 的内存缓存即可覆盖“重复取图不重解码”;没有磁盘缓存需求(包内文件天然持久)。
  • 网络图:UIImage 体系不碰网络;要么 URLSession 取字节后自己解码,要么走第三方库(见 §1.5)。

1.3 解码降采样:CGImageSourceCreateThumbnailAtIndex(对应 Android inSampleSize / Flutter cacheWidth) ​

对应 Lab:ios-image-loading-sim —— 验证「4032×3024×4 = 46.5MB 全量解码 vs maxPixelSize=360 降采样 0.37MB」的峰值差异,以及模拟 NSCache 双限 LRU 的逐出顺序与内存警告清空。

ImageIO 可以直接在解码阶段生成指定最大边长的缩略图,内存峰值 = 缩略图大小,与 Android 解码期降采样同一思路:

swift
let options: [CFString: Any] = [
    kCGImageSourceCreateThumbnailFromImageAlways: true, // 从原图生成缩略图(而非读已有缩略图)
    kCGImageSourceThumbnailMaxPixelSize: 360,           // 限制最长边像素 → 等比降采样
    kCGImageSourceCreateThumbnailWithTransform: true,   // 按 EXIF 方向旋转后输出
    kCGImageSourceShouldCacheImmediately: true          // 立即解码进内存,避免首帧卡顿
]
if let source = CGImageSourceCreateWithURL(fileURL as CFURL, nil),
   let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) {
    let uiImage = UIImage(cgImage: cgImage)
}

iOS 15+ 更简单的等价物:image.preparingThumbnail(of: CGSize)(内部就是同一套 ImageIO 降采样),适合“拿到 UIImage 后再出小图”的场景。

1.4 显示适配:UIImageView.contentMode ​

contentMode行为适用
scaleAspectFill等比拉伸填满并裁切超出部分全屏背景 / Banner(对应 12/01 的 iOS 大图策略)
scaleAspectFit等比完整显示,留黑边图片查看器 / 聊天大图
scaleToFill不保比拉伸少用,会变形
center / scaleAspectFit 组合不放大原尺寸缩略图
swift
imageView.contentMode = .scaleAspectFill  // 大图等比铺满 + 裁切
imageView.clipsToBounds = true            // 超出部分裁掉

1.5 磁盘缓存:官方没有,第三方补齐 ​

  • URLCache:HTTP 层缓存,按响应头(Cache-Control / ETag)工作,可做基本磁盘缓存;
  • SDWebImage / Kingfisher:主流网络图库,内存(NSCache)+ 磁盘两级 + 占位 / 进度 / 失败回调,对应 Android Glide / Coil 与 Flutter CachedNetworkImage。
swift
// Kingfisher:内存 + 磁盘两级缓存 + 占位
imageView.kf.setImage(with: url, placeholder: UIImage(named: "placeholder"))

1.6 超大图:CATiledLayer 分块(对应 Android BitmapRegionDecoder / Flutter tile) ​

  • 万级像素全景图 / 地图用 UIScrollView + CATiledLayer:按当前缩放级别只绘制可见瓦片,内存恒定 ≈ 视口大小,与整图尺寸无关——与 07/08 §6 的 BitmapRegionDecoder、11/04 六层方案 L6 是同一套思想。

2. 特点与优势 ​

维度iOS (UIKit)AndroidFlutter
资源分档@1x/@2x/@3x 文件名 / Single Scale密度桶 drawable-{桶} / nodpiasset 变体 1.0x/2.0x/3.0x
缺失行为无该档则找次近档(UIKit 内部回退)按 density 缩放(可能放大)不缩放(原像素直用)
解码降采样CGImageSourceCreateThumbnailAtIndex / preparingThumbnailinSampleSize / setTargetSizecacheWidth / cacheHeight
内存缓存系统 NSCache(UIImage(named:))Glide BitmapPool / Coil MemoryCacheImageCache(双限)
磁盘缓存官方无(URLCache / 第三方)Glide / Coil / Fresco 自带CachedNetworkImage
复用池无 inBitmap 等价物(无显式池 API)inBitmap / BitmapPool无 native 池
超大图CATiledLayerBitmapRegionDecodertile 库(如 interactive_image)

3. 用法 ​

3.1 背景大图:Single Scale + Aspect Fill(对应 12/01 大图策略) ​

  1. Asset Catalog 图片集选 Single Scale,放一张原始高清图;
  2. 代码里 UIImageView + contentMode = .scaleAspectFill 等比铺满。

3.2 网络大图降采样后再展示 ​

swift
// URLSession 拿到 Data → ImageIO 降采样到屏幕尺寸 → UIImageView
let options: [CFString: Any] = [
    kCGImageSourceCreateThumbnailFromImageAlways: true,
    kCGImageSourceThumbnailMaxPixelSize: max(Int(UIScreen.main.bounds.width * UIScreen.main.scale), 1),
    kCGImageSourceCreateThumbnailWithTransform: true
]
let source = CGImageSourceCreateWithData(data as CFData, nil)!
let cg = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)!
imageView.image = UIImage(cgImage: cg)

3.3 选型速查 ​

场景首选
本地图标 / 小图UIImage(named:) + @2x/@3x
本地大背景Asset Catalog Single Scale + scaleAspectFill
网络图Kingfisher / SDWebImage(内存 + 磁盘 + 占位)
超大图 / 全景CATiledLayer
聊天 / 查看器大图scaleAspectFit + 双击缩放(UIScrollView)

本页回答的问题(一句话答) ​

  • iOS 为什么不需要密度桶?
    • 一句话答:设备 scale 只有 1x/2x/3x 三档离散值,UIImage(named:) 按文件名后缀精确匹配、零重采样,不需要 Android 那种「连续密度 → 桶 + 解码缩放」。
  • UIImage(named:) 和 UIImage(contentsOfFile:) 有什么区别?
    • 一句话答:前者走系统级内存缓存(同名重复取图不重解码),后者无缓存每次都解码;本地资源用 named,临时文件用 contentsOfFile。
  • CGImageSourceCreateThumbnailAtIndex 对应 Android / Flutter 的什么?
    • 一句话答:解码期降采样——对应 Android inSampleSize / setTargetSize 与 Flutter cacheWidth/cacheHeight,都在解码阶段把像素降到目标尺寸,控内存不靠显示缩放(详见 07/08 §5)。
  • iOS 大背景图放哪?怎么铺满?
    • 一句话答:Asset Catalog 设 Single Scale 放一张原始高清图(不勾 @2x/@3x 细分),代码 contentMode = .scaleAspectFill 等比铺满裁切(对应 12/01 §4)。
  • iOS 有没有官方磁盘缓存?
    • 一句话答:没有专门图片磁盘缓存;本地资源在包内天然持久,网络图靠 URLCache(HTTP 层)或第三方库(SDWebImage / Kingfisher)补内存 + 磁盘两级。

对应实验 ​

Lab说明源码
ios-image-loading-sim纯数据结构模拟(无需 Xcode/模拟器):解码内存公式 48771072 字节、maxPixelSize 解码期降采样 388800 字节、SimNSCache 双限 LRU 逐出 [icon, banner] + 内存警告清空,全部数值可断言(与文首「代码索引」一致)ImageLoadingSim.swift · verify.sh

后续若建立 iOS Host 工程,可再补「Single Scale 大图 + scaleAspectFill」最小演示页并回链本页。

复习检查题 ​

  1. iOS 为什么不需要密度桶?与 Android 的机制差异在哪?

    答:iOS 设备 scale 离散(@1x/@2x/@3x 三档),UIImage(named:) 按文件名后缀精确匹配、1:1 使用;Android 设备密度连续(如 420dpi → s=2.625),必须用桶 + inDensity/inTargetDensity 解码缩放来保证 dp 物理尺寸一致。

  2. UIImage(named:) 有缓存吗?UIImage(contentsOfFile:) 呢?

    答:前者有系统级内存缓存(NSCache,按名字),重复取图不重解码;后者无缓存,每次重新解码。

  3. 大图降采样为什么要用 CGImageSourceCreateThumbnailAtIndex 而不是直接 UIImage(data:) 再缩放?

    答:UIImage(data:) 先整图解码进内存(峰值 = 原图),再缩放是“先全图再缩”的高峰路径;CGImageSourceCreateThumbnailAtIndex 在解码阶段直接输出目标边长的小图,内存峰值 = 小图(对应 Android inSampleSize 与 Flutter cacheWidth)。

  4. iOS 全屏背景大图在 Asset Catalog 里应该怎么配置?为什么?

    答:设 Single Scale(单源)放原始高清图,不按 @2x/@3x 分档;这样只有一份资源、包体不翻倍,显示时用 scaleAspectFill 等比铺满裁切即可(对应 12/01 的“大图单源 + 运行时自适应”)。

  5. iOS 有没有类似 Android inBitmap 的复用池?

    答:没有显式等价物。UIKit 用系统 NSCache 做内存缓存,但没有 BitmapPool 那样的显式复用 API;因此频繁加载大图的峰值内存主要靠“解码期降采样 + 缓存”控制。

速记 ​

  • 资源:@1x/@2x/@3x 文件名后缀 = 离散 scale 精确匹配;大图用 Single Scale。
  • 降采样:CGImageSourceCreateThumbnailAtIndex(MaxPixelSize)≈ Android inSampleSize ≈ Flutter cacheWidth。
  • 铺满:contentMode = .scaleAspectFill + clipsToBounds。
  • 缓存:内存 = 系统 NSCache;磁盘 = URLCache / Kingfisher / SDWebImage(官方无)。
  • 超大图:CATiledLayer 分块,内存恒定 ≈ 视口。

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