做者:Mattt,原文连接,原文日期:2019-05-06 译者:ericchuhong;校对:numbbbbb,WAMaker;定稿:Pancfphp
长期以来,iOS 开发人员一直被一个奇怪的问题困扰着:html
“如何对一张图像进行渲染优化?”git
这个使人困扰的问题,是因为开发者和平台的相互不信任引发的。各类各样的代码示例充斥着 Stack Overflow,每一个人都声称只有本身的方法是真正的解决方案 —— 而别人的是错的。github
在本周的文章中,咱们将介绍 5 种不一样的 iOS 图像渲染优化技巧(在 MacOS 上时适当地将 UIImage
转换成 NSImage
)。相比于对每一种状况都规定一种方法,咱们将从人类工程学和性能表现方面进行衡量,以便你更好地理解什么时该用哪种,不应用哪一些。编程
你能够本身下载、构建和运行 示例项目代码 来试验这些图像渲染优化技巧。swift
在开始以前,让咱们先讨论一下为何须要对图像进行渲染优化。毕竟,UIImageView
会自动根据 contentmode
属性 规定的行为缩放和裁剪图像。在绝大多数状况下,.scaleAspectFit
、.scaleAspectFill
或 .scaleToFill
已经彻底知足你的所需。vim
imageView.contentMode = .scaleAspectFit
imageView.image = image
复制代码
那么,何时对图像进行渲染优化才有意义呢?
当它明显大于 UIImageView
显示尺寸的时候缓存
看看来自 NASA 视觉地球相册集锦 的这张 使人赞叹的图片:网络
想要完整渲染这张宽高为 12,000 px 的图片,须要高达 20 MB 的空间。对于当今的硬件来讲,你可能不会在乎这么少兆字节的占用。但那只是它压缩后的尺寸。要展现它,UIImageView
首先须要把 JPEG 数据解码成位图(bitmap),若是要在一个 UIImageView
上按原样设置这张全尺寸图片,你的应用内存占用将会激增到几百兆,对用户明显没有什么好处(毕竟,屏幕能显示的像素有限)。但只要在设置 UIImageView
的 image
属性以前,将图像渲染的尺寸调整成 UIImageView
的大小,你用到的内存就会少一个数量级:闭包
内存消耗 (MB) | |
---|---|
无下采样 | 220.2 |
下采样 | 23.7 |
这个技巧就是众所周知的下采样(downsampling),在这些状况下,它能够有效地优化你应用的性能表现。若是你想了解更多关于下采样的知识或者其它图形图像的最佳实践,请参照 来自 WWDC 2018 的精彩课程。
而如今,不多有应用程序会尝试一次性加载这么大的图像了,可是也跟我从设计师那里拿到的图片资源不会差太多。(认真的吗?一张颜色渐变的 PNG 图片要 3 MB?) 考虑到这一点,让咱们来看看有什么不一样的方法,可让你用来对图像进行优化或者下采样。
不用说,这里全部从 URL 加载的示例图像都是针对本地文件。记住,在应用的主线程同步使用网络请求图像毫不是什么好主意。
优化图像渲染的方法有不少种,每种都有不一样的功能和性能特性。咱们在本文看到的这些例子,架构层次跨度上从底层的 Core Graphics、vImage、Image I/O 到上层的 Core Image 和 UIKit 都有。
为了统一调用方式,如下的每种技术共用一个公共接口方法:
func resizedImage(at url: URL, for size: CGSize) -> UIImage? { <#...#> }
imageView.image = resizedImage(at: url, for: size)
复制代码
这里,size
的计量单位不是用 pixel
,而是用 point
。想要计算出你调整大小后图像的等效尺寸,用主 UIScreen
的 scale
,等比例放大你 UIImageView
的 size
大小:
let scaleFactor = UIScreen.main.scale
let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
let size = imageView.bounds.size.applying(scale)
复制代码
若是你是在异步加载一张大图,使用一个过渡动画让图像逐渐显示到
UIImageView
上。例如:
class ViewController: UIViewController {
@IBOutlet var imageView: UIImageView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let url = Bundle.main.url(forResource: "Blue Marble West",
withExtension: "tiff")!
DispatchQueue.global(qos: .userInitiated).async {
let image = resizedImage(at: url, for: self.imageView.bounds.size)
DispatchQueue.main.sync {
UIView.transition(with: self.imageView,
duration: 1.0,
options: [.curveEaseOut, .transitionCrossDissolve],
animations: {
self.imageView.image = image
})
}
}
}
}
复制代码
图像渲染优化的最上层 API 位于 UIKit 框架中。给定一个 UIImage
,你能够绘制到 UIGraphicsImageRenderer
的上下文(context)中以渲染缩小版本的图像:
import UIKit
// 技巧 #1
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
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))
}
}
复制代码
UIGraphicsImageRenderer
是一项相对较新的技术,在 iOS 10 中被引入,用以取代旧版本的 UIGraphicsBeginImageContextWithOptions
/ UIGraphicsEndImageContext
API。你经过指定以 point
计量的 size
建立了一个 UIGraphicsImageRenderer
。image
方法带有一个闭包参数,返回的是一个通过闭包处理后的位图。最终,原始图像便会在缩小到指定的范围内绘制。
在不改变图像原始纵横比(aspect ratio)的状况下,缩小图像原始的尺寸来显示一般颇有用。
AVMakeRect(aspectRatio:insideRect:)
是在 AVFoundation 框架中很方便的一个函数,负责帮你作以下的计算:
import func AVFoundation.AVMakeRect
let rect = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
复制代码
Core Graphics / Quartz 2D 提供了一系列底层 API 让咱们能够进行更多高级的配置。
给定一个 CGImage
做为暂时的位图上下文,使用 draw(_:in:)
方法来绘制缩放后的图像:
import UIKit
import CoreGraphics
// 技巧 #2
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
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: image.bytesPerRow,
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)
}
复制代码
这个 CGContext
初始化方法接收了几个参数来构造一个上下文,包括了必要的宽高参数,还有在给出的色域范围内每一个颜色通道所须要的内存大小。在这个例子中,这些参数都是经过 CGImage
这个对象获取的。下一步,设置 interpolationQuality
属性为 .high
指示上下文在保证必定的精度上填充像素。draw(_:in:)
方法则是在给定的宽高和位置绘制图像,可让图片在特定的边距下裁剪,也能够适用于一些像是人脸识别之类的图像特性。最后 makeImage()
从上下文获取信息而且渲染到一个 CGImage
值上(以后会用来构造 UIImage
对象)。
Image I/O 是一个强大(却鲜有人知)的图像处理框架。抛开 Core Graphics 不说,它能够读写许多不一样图像格式,访问图像的元数据,还有执行常规的图像处理操做。这个框架经过先进的缓存机制,提供了平台上最快的图片编码器和解码器,甚至能够增量加载图片。
这个重要的 CGImageSourceCreateThumbnailAtIndex
提供了一个带有许多不一样配置选项的 API,比起在 Core Graphics 中等价的处理操做要简洁得多:
import ImageIO
// 技巧 #3
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
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)
}
复制代码
给定一个 CGImageSource
和一系列配置选项,CGImageSourceCreateThumbnailAtIndex(_:_:_:)
函数建立了一个图像的缩略图。优化尺寸大小的操做是经过 kCGImageSourceThumbnailMaxPixelSize
完成的,它根据图像原始宽高比指定的最大尺寸来缩放图像。经过设定 kCGImageSourceCreateThumbnailFromImageIfAbsent
或 kCGImageSourceCreateThumbnailFromImageAlways
选项,Image I/O 能够自动缓存优化后的结果以便后续调用。
Core Image 内置了 Lanczos 重采样(resampling) 功能,它是以 CILanczosScaleTransform
的同名滤镜命名的。虽然能够说它是在 UIKit 层级之上的 API,但无处不在的 key-value 编写方式致使它使用起来很不方便。
即使如此,它的处理模式仍是一致的。
建立转换滤镜,对滤镜进行配置,最后渲染输出图像,这样的步骤和其余任何 Core Image 的工做流没什么不一样。
import UIKit
import CoreImage
let sharedContext = CIContext(options: [.useSoftwareRenderer : false])
// 技巧 #4
func resizedImage(at url: URL, scale: CGFloat, aspectRatio: CGFloat) -> UIImage? {
guard let image = CIImage(contentsOf: url) else {
return nil
}
let filter = CIFilter(name: "CILanczosScaleTransform")
filter?.setValue(image, forKey: kCIInputImageKey)
filter?.setValue(scale, forKey: kCIInputScaleKey)
filter?.setValue(aspectRatio, forKey: kCIInputAspectRatioKey)
guard let outputCIImage = filter?.outputImage,
let outputCGImage = sharedContext.createCGImage(outputCIImage,
from: outputCIImage.extent)
else {
return nil
}
return UIImage(cgImage: outputCGImage)
}
复制代码
这个名叫 CILanczosScaleTransform
的 Core Image 滤镜分别接收了 inputImage
、inputScale
和 inputAspectRatio
三个参数,每个参数的意思也都不言自明。
更有趣的是,CIContext
在这里被用来建立一个 UIImage
(间接经过 CGImageRef
表示),由于 UIImage(CIImage:)
常常不能按咱们本意使用。建立 CIContext
是一个代价很昂贵的操做,因此使用上下文缓存以便重复的渲染工做。
一个
CIContext
可使用 GPU 或者 CPU(慢不少)渲染建立出来。经过指定构造方法中的.useSoftwareRenderer
选项来选择使用哪一个硬件。(提示:用更快的那个,你以为呢?)
最后一个了,它是古老的 Accelerate 框架 —— 更具体点来讲,它是 vImage
的图像处理子框架。
vImage 附带有 一些不一样的功能,能够用来裁剪图像缓冲区大小。这些底层 API 保证了高性能同时低能耗,但会致使你对缓冲区的管理操做增长(更不用说要编写更多的代码了):
import UIKit
import Accelerate.vImage
// 技巧 #5
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
// 解码源图像
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
}
// 定义图像格式
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
// 建立并初始化源缓冲区
var sourceBuffer = vImage_Buffer()
defer { sourceBuffer.data.deallocate() }
error = vImageBuffer_InitWithCGImage(&sourceBuffer,
&format,
nil,
image,
vImage_Flags(kvImageNoFlags))
guard error == kvImageNoError else { return nil }
// 建立并初始化目标缓冲区
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 }
// 优化缩放图像
error = vImageScale_ARGB8888(&sourceBuffer,
&destinationBuffer,
nil,
vImage_Flags(kvImageHighQualityResampling))
guard error == kvImageNoError else { return nil }
// 从目标缓冲区建立一个 CGImage 对象
guard let resizedImage =
vImageCreateCGImageFromBuffer(&destinationBuffer,
&format,
nil,
nil,
vImage_Flags(kvImageNoAllocate),
&error)?.takeRetainedValue(),
error == kvImageNoError
else {
return nil
}
return UIImage(cgImage: resizedImage)
}
复制代码
这里使用 Accelerate API 进行的明确操做,比起目前为止讨论到的其余优化方法更加底层。但暂时无论这些不友好的类型申明和函数名称的话,你会发现这个方法至关直接了当。
UIImage
对象。那么这些不一样的方法是如何相互对比的呢?
这个项目 是一些 性能对比 结果,运行环境是 iPhone 7 iOS 12.2。
下面的这些数字是屡次迭代加载、优化、渲染以前那张 超大地球图片 的平均时间:
耗时 (seconds) | |
---|---|
技巧 #1: UIKit |
0.1420 |
技巧 #2: Core Graphics 1 |
0.1722 |
技巧 #3: Image I/O |
0.1616 |
技巧 #4: Core Image 2 |
2.4983 |
技巧 #5: vImage |
2.3126 |
1 设置不一样的 CGInterpolationQuality
值出来的结果是一致的,在性能上的差别能够忽略不计。
2 若在 CIContext
建立时设置 kCIContextUseSoftwareRenderer
的值为 true
,会致使耗时相比基础结果慢一个数量级。
UIGraphicsImageRenderer
是你最佳的选择。vImage
,不然在大多数状况下用到底层的 Accelerate API 所需的额外工做多是不合理的。本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 swift.gg。