iOS图片优化
一、iOS图片渲染的3个核心步骤
想要优化图片,首先得搞懂iOS加载图片的底层流程——每一步都藏着内存优化的关键。加载一张图片,会经历3个阶段:
1. 加载(Load)
iOS先把压缩后的图片(比如我们例子中的266 KB)读入内存,这个阶段内存压力很小,基本不用在意。
2. 解码(Decode)
这是最关键的一步!iOS会把压缩的图片,转换成GPU能识别的格式——此时图片会被完全解压缩,内存占用瞬间飙升到我们计算的14 MB。 这个阶段,iOS会创建一个「图片缓冲区」,保存图片在内存中的完整数据。所以,内存大小只和图片尺寸相关,和文件大小毫无关系。
3. 渲染(Render)
图片数据准备就绪,开始渲染到屏幕上——哪怕你只用一个60×60点的小视图展示它。 补充一个关键知识点: 对于UIImage来说,它会保留解码后的缓冲区。因为渲染不是一次性操作(比如列表滚动时需要重复显示),保留缓冲区能避免重复解码,提升性能。但如果不优化,这个缓冲区就会一直占用大量内存。 还有一个重点:想要丝滑的60帧/秒滚动,iOS需要在1/60秒内完成视图渲染;ProMotion高刷屏设备,更是要求1/120秒。只要解码或渲染慢一点,就会掉帧,影响用户体验。
2、尺寸决定内存占用
let filePath =Bundle.main.path(forResource:"baylor", ofType: "jpg")!
let url =NSURL(fileURLWithPath: filePath)
let fileImage =UIImage(contentsOfFile: filePath)
// 图片视图
let imageView =UIImageView(image: fileImage)
imageView.translatesAutoresizingMaskIntoConstraints =false
imageView.contentMode = .scaleAspectFit
imageView.widthAnchor.constraint(equalToConstant: 300).isActive =true
imageView.heightAnchor.constraint(equalToConstant: 400).isActive =true
view.addSubview(imageView)
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive =true
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive =true
运行后,用LLDB调试工具查看图片实际尺寸,会发现: <UIImage: 0x600003d41a40>, {1718, 2048} 这里的尺寸单位是「点」,在2x(iPhone 8等)或3x(iPhone 13/14等)设备上,实际像素还要再乘以对应倍数,内存占用会更高。 用vmmap命令查看内存占用(终端输入): vmmap --summary baylor.memgraph 结果显示,应用总物理内存占用约69.5M;再过滤Image IO相关内存: vmmap --summary baylor.memgraph | grep "Image IO" 输出如下,刚好对应我们计算的14 MB左右: Image IO 13.4M 13.4M 13.4M 0K 0K 0K 0K 2 结论:即便只用300×400的小视图展示,你依然在为原图的全尺寸买单——这就是很多APP图片卡顿、崩溃的核心原因。
三、色彩空间
除了尺寸,「色彩空间」也会直接影响内存占用。 我们之前假设图片用的是sRGB格式(每个像素4字节),但如果你的设备支持「广色域」(比如iPhone 8+/X及以上机型),图片若采用广色域格式,内存占用会直接翻倍。 反过来,如果你只需要单色图片(比如红色圆形图标),用Metal的Alpha 8格式(单通道),内存占用会大幅降低。 这里给大家一个关键建议: 优先用UIGraphicsImageRenderer,替代UIGraphicsBeginImageContextWithOptions。 因为后者固定使用sRGB格式,要么错失广色域的显示效果,要么无法节省内存;而从iOS 12开始,UIGraphicsImageRenderer会自动选择最优的色彩空间,最高能节省75%的内存。 举个例子,绘制一个简单的红色圆形图标,两种API的对比:
旧API(4字节/像素,内存占用高):
let circleSize =CGSize(width: 60, height: 60)
UIGraphicsBeginImageContextWithOptions(circleSize, true, 0)
let ctx =UIGraphicsGetCurrentContext()!
UIColor.red.setFill()
ctx.addEllipse(in: CGRect(origin: .zero, size: circleSize))
ctx.drawPath(using: .fill)
let circleImage =UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
新API(自动优化,可低至1字节/像素)
let circleSize = CGSize(width: 60, height: 60)
let renderer = UIGraphicsImageRenderer(size: circleSize)
let circleImage = renderer.image { ctx in
UIColor.red.setFill()
ctx.cgContext.addEllipse(in: CGRect(origin: .zero, size: circleSize))
ctx.cgContext.drawPath(using: .fill)
}
四、核心优化:降采样(Downsampling)而非降尺寸
很多开发者会犯一个错误:用UIImage的缩放功能(降尺寸)来优化图片。但这样做没用——因为UIImage会先把原图完整解码到内存,再做缩放,内存开销一点没少。 正确的做法是:降采样(Downsampling)——直接生成一个匹配展示尺寸的图片,只承担缩放后图片的内存开销。 用底层API CGImageSource实现降采样,代码如下:
func downsampleImage(at url: URL, maxSize: CGFloat) -> UIImage {
let sourceOptions: [CFString: Any] = [kCGImageSourceShouldCache: false]
let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions as CFDictionary)!
let downsampleOptions: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: maxSize,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true
]
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions as CFDictionary)!
return UIImage(cgImage: cgImage)
}
视觉效果和之前完全一样,但内存占用大幅下降!用vmmap再次查看
两个关键注意点:
kCGImageSourceShouldCacheImmediately:控制解码时机,只有需要时才解码,避免提前占用内存;
五、配合预获取,后台解码
如果你的APP有列表(比如朋友圈、图片列表),建议把「降采样」和iOS 11推出的「预获取API」结合使用——提前加载并解码图片,避免滚动时卡顿。 注意:降采样会带来短暂的CPU峰值,建议放在「自定义串行队列」中处理,避免线程爆炸,同时把解码操作放到后台,不阻塞主线程。 给大家一个Objective-C的示例(适配老项目):
- (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths
{
if (self.downsampledImage != nil || self.listItem.mediaAssetData == nil) return;
NSIndexPath *targetIndexPath = [NSIndexPath indexPathForRow:0 inSection:SECTION_MEDIA];
if ([indexPaths containsObject:targetIndexPath]) {
CGFloat scale = tableView.traitCollection.displayScale;
CGFloat maxPixelSize = (tableView.bounds.size.width - margin) * scale;
dispatch_async(self.downsampleQueue, ^{
self.downsampledImage = [UIImage downsampledImageFromData:self.listItem.mediaAssetData
scale:scale
maxPixelSize:maxPixelSize];
dispatch_async(dispatch_get_main_queue(), ^{
self.listItem.downsampledMediaImage = self.downsampledImage;
});
});
}
}
最后一个小提示: 绝大多数图片资源,尽量用「Asset Catalog」(Xcode中的资源目录)管理,它会自动帮你优化缓冲区大小、适配不同设备,省掉很多手动优化的工作。
六、总结:
编程里总有你不知道的细节,图片处理就是最典型的例子——大多数时候,我们只是简单初始化一个UIImageView,就以为万事大吉。 现在的iPhone性能强、内存大,但这不是我们忽视内存优化的理由。别让一张自拍占用1 GB内存,导致APP被系统强行杀掉(jetsam机制)。 记住这几个核心优化点,就能写出更流畅、更稳定的iOS APP:
图片内存占用看「尺寸」,不是「文件大小」; 优先用UIGraphicsImageRenderer,自动优化色彩空间; 用降采样(CGImageSource)替代UIImage缩放,大幅节省内存; 列表图片配合预获取API,后台解码,避免卡顿。
夜雨聆风