概要
今天是 WWDC 2023 的第二天,我在疯狂刷讲座的过程中突然瞥到了一个之前没用见的 API:preparingThumbnail(of:)
,于是稍微研究了一下,感觉还是非常香的,于是记下来分享一下。
图片渲染机制(精简版)
了解 iOS 渲染优化的同学一定刷过 WWDC 18 的一个宝藏视频 Image and Graphics Best Practices (目前似乎已经下掉了,但是其他平台有,没有看过的同学必看!)。视频中提到,图片渲染的成本可能要比很多人想的大的多。很多人会觉得图片渲染占用的内存和图片占用的磁盘大小有关,其实完全不是。图片渲染的真实内容占用其实是和图片的尺寸有关。一个被压缩到极致的图片,经过解码之后,可能会是一个尺寸超级大的图片,而图片的每个像素点都占用固定的内存,图片尺寸越大则像素越多,进而占用的内存也就更大。
因此苹果建议,如果图片尺寸远大于实际渲染的尺寸的话,可以使用下采样的方式,只渲染一个小尺寸的图片。这样就能极大优化内容占用。
会有同学认为图片的渲染尺寸和 UIImageView 的尺寸一样,其实不是。虽然 UIImageView 限定了图片在屏幕上的渲染尺寸,但图片在内存中是按完整尺寸储存的。
优化图片渲染
读到这边很多同学可能就准备上手使用 UIGraphicsImageRenderer
把 UIImage
压缩到小尺寸了。但其实这种方式成本非常高,因为把图片读到 UIImage
的过程中其实已经是将完整图片尺寸解码出来了,经过 UIGraphicsImageRenderer
转换之后虽然能得到一个较小的图片,但是中间额外的解码压缩过程效率很低,还会造成内存波动。
苹果推荐使用 ImageIO
提供的更底层的 API 来渲染图片。这个 API 会直接从源文件中解码出指定尺寸的图片数据,因此效率更高。一句话总结就是速度更快、内存占用更小。
import UIKitimport ImageIOstruct ImageIOConverter{static func resize(url: URL)-> UIImage{guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else {fatalError("Can not get imageSource")}let options: [NSString: Any] = [ kCGImageSourceThumbnailMaxPixelSize: 300, kCGImageSourceCreateThumbnailFromImageAlways: true ]guard let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else {fatalError("Can not get scaledImage")}return UIImage(cgImage: scaledImage)}}import UIKit import ImageIO struct ImageIOConverter{ static func resize(url: URL)-> UIImage{ guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else { fatalError("Can not get imageSource") } let options: [NSString: Any] = [ kCGImageSourceThumbnailMaxPixelSize: 300, kCGImageSourceCreateThumbnailFromImageAlways: true ] guard let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { fatalError("Can not get scaledImage") } return UIImage(cgImage: scaledImage) } }import UIKit import ImageIO struct ImageIOConverter{ static func resize(url: URL)-> UIImage{ guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else { fatalError("Can not get imageSource") } let options: [NSString: Any] = [ kCGImageSourceThumbnailMaxPixelSize: 300, kCGImageSourceCreateThumbnailFromImageAlways: true ] guard let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { fatalError("Can not get scaledImage") } return UIImage(cgImage: scaledImage) } }
不过 ImageIO
毕竟是偏底层的框架,代码的复杂度也很高。于是 Apple 在 WWDC21 推出了 preparingThumbnail
方法,可以方便快捷的生成指定大小的图片,建议能用上的地方都用上。
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? ItemCell else {fatalError("Unexpected type for cell. Check configuration.")}let item = items[indexPath.item] cell.nameLabel?.text = item.namelet thumbnail = item.image.preparingThumbnail(of: thumbnailSize)cell.thumbnailImageView?.image = thumbnail return cell}func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? ItemCell else { fatalError("Unexpected type for cell. Check configuration.") } let item = items[indexPath.item] cell.nameLabel?.text = item.name let thumbnail = item.image.preparingThumbnail(of: thumbnailSize) cell.thumbnailImageView?.image = thumbnail return cell }func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? ItemCell else { fatalError("Unexpected type for cell. Check configuration.") } let item = items[indexPath.item] cell.nameLabel?.text = item.name let thumbnail = item.image.preparingThumbnail(of: thumbnailSize) cell.thumbnailImageView?.image = thumbnail return cell }
这个 API 还贴心的准备了三个版本:
- 同步版:
preparingThumbnail(of:)
- 异步版:
prepareThumbnail(of:completionHandler:)
- 协程版:
byPreparingThumbnail(ofSize:)
但坏消息是只支持 iOS 15.0+ 。
另外 SwiftUI 这边也没有发现类似的 API ,看来只能用 UIImage 曲线救国了。
总结
当我发现这么重要的 API 竟然是 21 年发布的还是挺吃惊的,隔壁 Flutter 很早就上了 ResizeImage
,做的也是一样的事情。
另外现在很多公司为了优化图片渲染都会考虑在云端对图片做裁剪,但一来服务器成本变高,二来 CDN 命中率也会下降。但好处是图片渲染压力变小了,带宽流量也少了。两种方案都是对渲染优化帮助很大的,大家可以借鉴一下。