Appearance
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 双限 LRU | ios-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) | Android | Flutter |
|---|---|---|---|
| 资源分档 | @1x/@2x/@3x 文件名 / Single Scale | 密度桶 drawable-{桶} / nodpi | asset 变体 1.0x/2.0x/3.0x |
| 缺失行为 | 无该档则找次近档(UIKit 内部回退) | 按 density 缩放(可能放大) | 不缩放(原像素直用) |
| 解码降采样 | CGImageSourceCreateThumbnailAtIndex / preparingThumbnail | inSampleSize / setTargetSize | cacheWidth / cacheHeight |
| 内存缓存 | 系统 NSCache(UIImage(named:)) | Glide BitmapPool / Coil MemoryCache | ImageCache(双限) |
| 磁盘缓存 | 官方无(URLCache / 第三方) | Glide / Coil / Fresco 自带 | CachedNetworkImage |
| 复用池 | 无 inBitmap 等价物(无显式池 API) | inBitmap / BitmapPool | 无 native 池 |
| 超大图 | CATiledLayer | BitmapRegionDecoder | tile 库(如 interactive_image) |
3. 用法
3.1 背景大图:Single Scale + Aspect Fill(对应 12/01 大图策略)
- Asset Catalog 图片集选 Single Scale,放一张原始高清图;
- 代码里
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 那种「连续密度 → 桶 + 解码缩放」。
- 一句话答:设备 scale 只有
UIImage(named:)和UIImage(contentsOfFile:)有什么区别?- 一句话答:前者走系统级内存缓存(同名重复取图不重解码),后者无缓存每次都解码;本地资源用 named,临时文件用 contentsOfFile。
CGImageSourceCreateThumbnailAtIndex对应 Android / Flutter 的什么?- 一句话答:解码期降采样——对应 Android
inSampleSize/setTargetSize与 FluttercacheWidth/cacheHeight,都在解码阶段把像素降到目标尺寸,控内存不靠显示缩放(详见 07/08 §5)。
- 一句话答:解码期降采样——对应 Android
- iOS 大背景图放哪?怎么铺满?
- 一句话答:Asset Catalog 设 Single Scale 放一张原始高清图(不勾
@2x/@3x细分),代码contentMode = .scaleAspectFill等比铺满裁切(对应 12/01 §4)。
- 一句话答:Asset Catalog 设 Single Scale 放一张原始高清图(不勾
- 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」最小演示页并回链本页。
复习检查题
iOS 为什么不需要密度桶?与 Android 的机制差异在哪?
答:iOS 设备 scale 离散(
@1x/@2x/@3x三档),UIImage(named:)按文件名后缀精确匹配、1:1 使用;Android 设备密度连续(如 420dpi → s=2.625),必须用桶 +inDensity/inTargetDensity解码缩放来保证 dp 物理尺寸一致。UIImage(named:)有缓存吗?UIImage(contentsOfFile:)呢?答:前者有系统级内存缓存(NSCache,按名字),重复取图不重解码;后者无缓存,每次重新解码。
大图降采样为什么要用
CGImageSourceCreateThumbnailAtIndex而不是直接UIImage(data:)再缩放?答:
UIImage(data:)先整图解码进内存(峰值 = 原图),再缩放是“先全图再缩”的高峰路径;CGImageSourceCreateThumbnailAtIndex在解码阶段直接输出目标边长的小图,内存峰值 = 小图(对应 AndroidinSampleSize与 FluttercacheWidth)。iOS 全屏背景大图在 Asset Catalog 里应该怎么配置?为什么?
答:设 Single Scale(单源)放原始高清图,不按
@2x/@3x分档;这样只有一份资源、包体不翻倍,显示时用scaleAspectFill等比铺满裁切即可(对应 12/01 的“大图单源 + 运行时自适应”)。iOS 有没有类似 Android
inBitmap的复用池?答:没有显式等价物。UIKit 用系统
NSCache做内存缓存,但没有BitmapPool那样的显式复用 API;因此频繁加载大图的峰值内存主要靠“解码期降采样 + 缓存”控制。
速记
- 资源:
@1x/@2x/@3x文件名后缀 = 离散 scale 精确匹配;大图用 Single Scale。 - 降采样:
CGImageSourceCreateThumbnailAtIndex(MaxPixelSize)≈ AndroidinSampleSize≈ FluttercacheWidth。 - 铺满:
contentMode = .scaleAspectFill+clipsToBounds。 - 缓存:内存 = 系统
NSCache;磁盘 =URLCache/ Kingfisher / SDWebImage(官方无)。 - 超大图:
CATiledLayer分块,内存恒定 ≈ 视口。