iOS 调整图片 tips

最近写一个 Demo 时,需要用到一些本地的大图,发现在使用时能明显感觉到操作有一个卡卡的。解决这个小问题时,顺带将加载图片的流程复习了一下。

图片显示流程

原文来自于FastImageCache中,下面是我翻译的中文:

  1. UIImage(contentsOfFile: path) 使用 Image I/O 从内存映射数据创建 CGImageRef。在这时,图像尚未被解码。
  2. 将生成的 UIImage 赋值给 imageView.image。
  3. 隐式 CATransaction 捕获到 imageView 图层树的变化。
  4. 在主线程的下一个 Runloop 到来时,Core Animation 提交隐式事务,这可能会创建被设置图层内容的副本,根据设置的图片,复制操作可能涉及以下部分或全部步骤:
    • 分配缓冲区来管理文件 IO 和解压缩操作。
    • 文件数据从磁盘读取到内存中。
    • 压缩的图像数据被解码成其未压缩的位图形式,这通常是非常耗费 CPU 的操作。
    • Core Animation 使用未压缩的位图数据来渲染图层。

在上述过程中,解码操作时非常耗时,常见的三方库的操作都是异步解码,然后返回主线程使用。

图片解码内存占用问题

不管是网络图片还是本地图片,由于一些图片的尺寸比较大,在渲染时会导致内存瞬间飙涨。

对于网络图片,有些存储服务会提供在图片链接后面拼接参数来获取小图,这样就能避免内存飙涨的问题。但是,如果存储后台不提供这种服务的话,就需要开发自己解决这个问题了。

对于本地图片来说也是一样的。

所以,对于大图来说,我们需要怎么做才能以更好的方式来加载呢?

来自官方的 WWDC2018 的 Image and Graphics Best Practices 中,提供了一种利用 ImageIO 生成缩略图来进行降采样的方式。

这里,我将几种方式,放在一起记录一下。

降采样方式

  • UIGraphicsImageRenderer
  • Core Graphics Context
  • Image I/O
  • Core Image Lanczos 重采样
  • vImage

下面几种方法中,图片原图尺寸 1200012000,渲染区域大小为屏幕上的 240240。

UIGraphicsImageRenderer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func resizedImage() -> UIImage? {
let url = Bundle.main.url(forResource: "VIIRS_3Feb2012_lrg", withExtension: "jpeg")!

let size = self.imageView.bounds.size // 240*240

guard let image = UIImage(contentsOfFile: url.path) else {
return nil
}

let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { (context) in
image.draw(in: CGRect(origin: .zero, size: size))
}
}

func image(actions: (UIGraphicsImageRendererContext) -> Void) -> UIImage 接受一个闭包参数并返回一个位图,该位图是执行传递的闭包的结果。在这种情况下,结果是按比例缩小以在指定范围内绘制的原始图像。

Core Graphics Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func resizedImage() -> UIImage? {
let url = Bundle.main.url(forResource: "VIIRS_3Feb2012_lrg", withExtension: "jpeg")!

let size = self.imageView.bounds.size // 240*240

guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
else {
return nil
}

let context = CGContext(data: nil,
width: Int(size.width),
height: Int(size.height),
bitsPerComponent: image.bitsPerComponent,
bytesPerRow: 0,
space: image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: image.bitmapInfo.rawValue)
context?.interpolationQuality = .high
context?.draw(image, in: CGRect(origin: .zero, size: size))

guard let scaledImage = context?.makeImage() else { return nil }

return UIImage(cgImage: scaledImage)
}

Image I/O

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func resizedImage() -> UIImage? {
let url = Bundle.main.url(forResource: "VIIRS_3Feb2012_lrg", withExtension: "jpeg")!

let size = self.imageView.bounds.size // 240*240

let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height)
]

guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)
else {
return nil
}

return UIImage(cgImage: image)
}

Core Image Lanczos 重采样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import CoreImage

// 声明一个实例变量,存储供复用
let sharedContext = CIContext(options: [.useSoftwareRenderer : false])

func resizedImage() -> UIImage? {
let url = Bundle.main.url(forResource: "VIIRS_3Feb2012_lrg", withExtension: "jpeg")!

let scale = 240.0 / 12000.0 // 缩小比例

guard let image = CIImage(contentsOf: url) else {
return nil
}

let filter = CIFilter(name: "CILanczosScaleTransform")
filter?.setValue(image, forKey: kCIInputImageKey)
filter?.setValue(scale, forKey: kCIInputScaleKey)

guard let outputCIImage = filter?.outputImage,
let outputCGImage = sharedContext.createCGImage(outputCIImage,
from: outputCIImage.extent)
else {
return nil
}

return UIImage(cgImage: outputCGImage)
}

vImage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import Accelerate.vImage

func resizedImage() -> UIImage? {
let url = Bundle.main.url(forResource: "VIIRS_3Feb2012_lrg", withExtension: "jpeg")!

let size = self.imageView.bounds.size // 240*240

// Decode the source image
guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil),
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let imageWidth = properties[kCGImagePropertyPixelWidth] as? vImagePixelCount,
let imageHeight = properties[kCGImagePropertyPixelHeight] as? vImagePixelCount
else {
return nil
}

// Define the image format
var format = vImage_CGImageFormat(bitsPerComponent: 8,
bitsPerPixel: 32,
colorSpace: nil,
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue),
version: 0,
decode: nil,
renderingIntent: .defaultIntent)

var error: vImage_Error

// Create and initialize the source buffer
var sourceBuffer = vImage_Buffer()
defer { sourceBuffer.data.deallocate() }
error = vImageBuffer_InitWithCGImage(&sourceBuffer,
&format,
nil,
image,
vImage_Flags(kvImageNoFlags))
guard error == kvImageNoError else { return nil }

// Create and initialize the destination buffer
var destinationBuffer = vImage_Buffer()
error = vImageBuffer_Init(&destinationBuffer,
vImagePixelCount(size.height),
vImagePixelCount(size.width),
format.bitsPerPixel,
vImage_Flags(kvImageNoFlags))
guard error == kvImageNoError else { return nil }

// Scale the image
error = vImageScale_ARGB8888(&sourceBuffer,
&destinationBuffer,
nil,
vImage_Flags(kvImageHighQualityResampling))
guard error == kvImageNoError else { return nil }

// Create a CGImage from the destination buffer
guard let resizedImage =
vImageCreateCGImageFromBuffer(&destinationBuffer,
&format,
nil,
nil,
vImage_Flags(kvImageNoAllocate),
&error)?.takeRetainedValue(),
error == kvImageNoError
else {
return nil
}

return UIImage(cgImage: resizedImage)
}

这个方式,看起来更加的繁琐,一个 API 看起来也不是非常易用。总结这个流程还是很清晰的:

  • 首先,从您的输入图像创建一个源缓冲区,
  • 然后,创建一个目标缓冲区来保存缩放后的图像
  • 接下来,将源缓冲区中的图像数据缩放到目标缓冲区,
  • 最后,从目标缓冲区中的结果图像数据创建图像。

显示方式

1
2
3
4
5
6
7
8
9
10
11
let url = Bundle.main.url(forResource: "VIIRS_3Feb2012_lrg", withExtension: "jpeg")!

let size = self.imageView.bounds.size

let begin = CFAbsoluteTimeGetCurrent()
let image = self.resizedImage(at: url, for: size) // 统一了一下入口
let end = CFAbsoluteTimeGetCurrent()

print("加载用时 - \(end - begin)")

self.imageView.image = image

总结

UIImage(named: “”) 方式直接加兹安

  • 十次加载平均耗时(s)
UIGraphicsImageRenderer Core Graphics Context Image I/O Core Image Lanczos 重采样 vImage
0.30405219793319704 0.3035615921020508 0.30096609592437745 1.2386974096298218 0.6203658103942871
  • vImage 方式也会造成内存瞬间飙升,但马上会恢复正常。
  • 肉眼看起来的话,UIGraphicsImageRenderer 和 Core Image 的方式最为清晰。

好了,这篇文章就到这里了。感兴趣的同学可以自己复制代码,换不同图片测试感受一下。