做者: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。