WWDC2018 影象最佳實踐

方秋枋發表於2018-06-08

Session: WWDC2018 Image and Graphics Best Practices

這個 Session 主要介紹了影象渲染管線,快取區,解碼,影象來源,自定義繪製和離屏繪製。通過學習該 Session,能夠對影象渲染流程有更清晰的認識,同時瞭解如何在開發中提高影象渲染的效能。

1. 影象渲染管線 (Image Rendering Pipeline)

從 MVC 架構的角度來說,UIImage 代表了 Model,UIImageView 代表了 View. 那麼渲染的過程我們可以這樣很簡單的表示:

Model 負責載入資料,View 負責展示資料。

但實際上,渲染的流程還有一個很重要的步驟:解碼(Decode)。

為了瞭解Decode,首先我們需要了解Buffer這個概念。

2. 緩衝區 (Buffers)

Buffer 在電腦科學中,通常被定義為一段連續的記憶體,作為某種元素的佇列來使用。

下面讓我們來了解幾種不同型別的 Buffer。

Image Buffers 代表了圖片(Image)在記憶體中的表示。每個元素代表一個畫素點的顏色,Buffer 大小與影象大小成正比.

The frame buffer 代表了一幀在記憶體中的表示。

Data Buffers 代表了圖片檔案(Image file)在記憶體中的表示。這是圖片的後設資料,不同格式的圖片檔案有不同的編碼格式。Data Buffers不直接描述畫素點。 因此,Decode這一流程的引入,正是為了將Data Buffers轉換為真正代表畫素點的Image Buffer

因此,影象渲染管線,實際上是像這樣的:

3. 解碼(Decoding)

Data Buffers 解碼到 Image Buffers 是一個CPU密集型的操作。同時它的大小是和與原始影象大小成比例,和 View 的大小無關。

想象一下,如果一個瀏覽照片的應用展示多張照片時,沒有經過任何處理,就直接讀取圖片,然後來展示。那 Decode 時,將會佔用極大的記憶體和 CPU。而我們展示的圖片的 View 的大小,其實是完全用不到這麼大的原始影象的。

如何解決這種問題呢? 我們可以通過 Downsampling 來解決,也即是生成縮圖的方式。

我們可以通過這段程式碼來實現:

func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {

	//生成CGImageSourceRef 時,不需要先解碼。
	let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
	let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!
	let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
	
	//kCGImageSourceShouldCacheImmediately 
	//在建立Thumbnail時直接解碼,這樣就把解碼的時機控制在這個downsample的函式內
	let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true,
								 kCGImageSourceShouldCacheImmediately: true,
								 kCGImageSourceCreateThumbnailWithTransform: true,
								 kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
	//生成
	let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
	return UIImage(cgImage: downsampledImage)
}複製程式碼

通過Downsampling,我們成功地減低了記憶體的使用,但是解碼同樣會耗費大量的 CPU 資源。如果使用者快速滑動介面,很有可能因為解碼而造成卡頓。

解決辦法:Prefetching + Background decoding

Prefetch 是 iOS10 之後加入到 TableView 和 CollectionView 的新技術。我們可以通過tableView(_:prefetchRowsAt:)這樣的介面提前準備好資料。有興趣的小夥伴可以搜一下相關知識。

至於Background decoding其實就是在子執行緒處理好解碼的操作。

let serialQueue = DispatchQueue(label: "Decode queue") func collectionView(_ collectionView: UICollectionView,
prefetchItemsAt indexPaths: [IndexPath]) {
	// Asynchronously decode and downsample every image we are about to show
	for indexPath in indexPaths {
		serialQueue.async {
			let downsampledImage = downsample(images[indexPath.row])
			DispatchQueue.main.async { self.update(at: indexPath, with: downsampledImage)
		}
	}
 }複製程式碼

值得注意的是,上面用了一條序列佇列來處理,這是為了避免Thread Explosion。執行緒的切換是昂貴的,如果同時開十幾,二十個執行緒來處理,會極大的拖慢處理速度。

4. 圖片來源(Image Sources)

我們的照片主要有四類來源

  1. Image Assets
  2. Bundle,Framework 裡面的圖片
  3. 在 Documents, Caches 目錄下的圖片
  4. 網路下載的資料

蘋果官方建議我們儘可能地使用 Image Assets, 因為蘋果做了很多相關優化,比如快取,每個裝置只打包自己使用到的圖片,從 iOS11 開始也支援了無損放大的向量圖。

5. 自定義繪製 (Custom Drawing)

只需要記住一個準則即可。除非萬不得已,不要過載drawRect函式。

因為過載drawRect函式會導致系統給UIView建立一個backing store, 畫素的數量是UIView大小乘以 contentsScale 的值。因此會耗費不少記憶體。

UIView 並不是一定需要一個backing store的,比如設定背景顏色就不需要(除非是 pattern colors)。如果需要設定複雜的背景顏色,可以直接通過 UIImageView 來實現。

6. 離屏繪製(Drawing Off-Screen)

如果我們想要自己建立Image Buffers, 我們通常會選擇使用UIGraphicsBeginImageContext(), 而蘋果的建議是使用UIGraphicsImageRenderer,因為它的效能更好,還支援廣色域。

總結

這個 Session 的時長只有三十多分鐘,因此主要從巨集觀角度為我們介紹了影象相關的知識。我們可以對它的每一個小章節都繼續深挖。

對圖形效能有興趣的同學,可以更深入地學習 WWDC 2014的 《Advanced Graphics and Animations for iOS Apps》。之前編寫的這篇WWDC心得與延伸:iOS圖形效能 也是很好的學習資料。