1. 程式人生 > >影象和圖形的最佳實踐(WWDC 2018 session 219)

影象和圖形的最佳實踐(WWDC 2018 session 219)

該篇部落格記錄觀看WWDC2018中Session219《Image And Graphics Best Practices》的內容及一些理解。

該Session主要講述了關於有效使用圖形內容的一些技術和策略。主要分三個方面:

  1. 從UIImage和UIImageView入手,講述UIKit對於圖形內容的處理。
  2. 討論使用UIKit高效的處理自定義繪圖。
  3. 簡單講述在應用中使用先進的CPU和GPU技術。

在整個討論中,主要討論圖形處理對於記憶體CPU的影響,這兩個因素可以影響到系統的反應速度以及電池的使用壽命。

UIImage和UIImageView

UIImage在UIKit中表示一個圖形的內容,而UIImageView在UIKit中用來呈現一個檢視。對應到MVC模式中,UIImage是一個載入圖形內容的model,而UIImageView是顯示渲染圖形的檢視。這兩者之間的關係是一種連續的一次性的簡單單向聯絡。如下圖所示:
UIImage與UIImageView關係

但是在這之外還有一個隱藏的、影響程式效能的過程,叫做解碼(Decode),如下圖:
Decode

Decode

在瞭解Decode的過程中,我們首先要了解一個概念:緩衝區(Buffer)。

  1. 緩衝區是在記憶體中連續的區域。
  2. 通常被視為元素序列。
    緩衝區(Buffer)

1.影象緩衝區(Image Buffer)

影象緩衝區中每個元素表示的是影象中一個畫素的顏色資訊。所以,該Buffer在記憶體中的大小與影象大小成正比。

2.幀緩衝區(Frame Buffer)

幀緩衝區是儲存應用實際呈現輸出的緩衝區。

在應用更新檢視層次結構時,UIKit會把應用程式的Window以及子檢視渲染到幀緩衝區中。這個更新頻率在iPhone上是60FPS,在iPad上是120FPS。
幀緩衝區(Frame Buffer)

3.資料緩衝區(Data Buffer)

資料緩衝區為儲存一系列bytes資料的緩衝區。

在影象例子中,資料緩衝區就是儲存從網路下載或者儲存在磁碟中的影象的資料,這些資料並不直接描述每一個畫素的資訊。
資料緩衝區(Data Buffer)

載入過程

現在,我們瞭解了Buffer以及幾種與影象相關的Buffer,接下來可以分析一下圖片載入過程了。

  1. 載入準備:我們準備一個影象元資料,一個UIImage,以及一個載入在檢視上的UIImageView,如下:
    載入準備
  2. 獲取畫素資訊:為了將影象中每個畫素資訊填充到Frame Buffer中,我們需要得到影象畫素資訊。UIImage會為我們處理這一點:UIImage會建立一個與圖片大小一致的Image Buffer用來儲存解碼(Decode)之後的影象畫素資訊。
    獲取畫素資訊
  3. 顯示在螢幕上:UIImageView讀取UIImage建立的Image Buffer資料,交由UIKit顯示。UIKit會對畫素資料縮放到顯示大小進行顯示。
    顯示在螢幕上

在載入過程中,UIKit會重複多次的要求UIImageView去進行渲染,這就造成了UIImage會多次的解碼Data Buffer。

為了解決這個問題,UIImage會只進行一次Data Buffer的解碼,在解碼之後,會掛起解碼後的Image Buffer。

所以,在這種情況下,應用對於每一個UIImage解碼後,都會在記憶體中掛起一個Image Buffer。

由於Image Buffer與影象大小成正比,所以在載入大圖片時,應用程式會在記憶體中掛起很多大的Image Buffer,這可能會造成作業系統介入,並最終殺死應用。

下采樣(downsampling)

針對於我們上述提到的問題,我們可以採用叫做下采用(downsampling)的技術。

我們注意到,我們最終顯示在螢幕上的檢視往往比實際圖片的尺寸要小,而通常情況下,Core Animation Framework會負責縮小影象。
縮放顯示

現在要做的,本質上是把縮放影象操作捕獲為一個叫縮圖的物件。由於減小了Image Buffer的大小,進而減小了在載入圖片中的使用的記憶體總大小。

同時,我們在解碼後,將圖片畫素資訊交給UIImageView去渲染至屏幕後,可以丟棄掉對應的Data Buffer,進一步減少記憶體的使用。

這個過程如下圖:
下采樣過程

接下來我們放在程式碼中進行一下分析,整體程式碼如下:
下采樣整體程式碼
接下來我們分析幾個重要的點:

  1. 建立CGImageSourceRef
    建立CGImageSourceRef
    在建立CGImageSourceRef的時候,CGImageSourceCreate方法可以接受一個選項設定,我們可以提供kCGImageSourceShouldCachefalse。這個選項設定告訴Core Graphics Frameword我們只是需要一個CGImageSourceRef來儲存對應路徑的檔案的資訊,而不需要立刻去對檔案中的影象進行解碼。

  2. 計算需要渲染至螢幕的真實尺寸
    計算需要渲染至螢幕的真實尺寸

  3. 獲取縮圖
    獲取縮圖
    在獲取縮圖的方法CGImageSourceCreateThumbnailAtIndex中,我們可以指定選項設定,其中需要我們關注的是kCGImageSourceShouldCacheImmediately(iOS7.0及以上版本),該設定告訴Core Graphics Frameword,在進行建立縮圖的時候,就應該立刻建立一個解碼後的Image Buffer。

通過以上的改進方式,可以大幅度的縮減記憶體的使用。以下為一組測試資料:
測試環境:Xcode9.4,iPhone 6s Plus, 5184*3456的5.1MB大小的JPG格式圖片。

  • 不使用改進方法,消耗記憶體為:49.3M
  • 使用改進方法,消耗記憶體為:9.4M

針對滾動檢視的優化

如果我們需要一個滾動檢視來展示一大組質量很大的圖片時,我們也會遇到記憶體過高的問題,接下來我們以UICollectionView載入圖片來做分析和優化。

使用下采樣(downsampling)的方式優化

首先我們採用我們上一步優化圖片的方式來為每一個cell中的圖片進行優化,程式碼可能如下:
下采樣優化滾動檢視

這樣看起來很有用,因為我們為每一個cell上的圖片顯示進行了優化,進而使得整體記憶體使用有一個明顯的降低。但是,這樣會引起另一個問題:如果此時滾動檢視,CPU可能會很快的將所需展示的內容渲染到幀緩衝區,但是如果滾動過快,CPU還需參與Core Graphics對於新影象的解幀,這個操作是非常耗時的,這樣就可能會造成在硬體讀取幀緩衝區資料顯示至螢幕時,幀緩衝區資料並沒有準備好,進而導致使用者視角中的卡頓。另外對於電池來說,在CPU不穩定時,可能會影響到電池使用壽命。

進一步優化

有兩種技術可以幫助我們解決這個問題。

  1. 預取(prefetching):在某一時刻,我們不需要某個cell,但是在不久的將來會需要這個cell,所以可以把某些工作提前至這個時刻來進行。

  2. 後臺執行(performing work in the background)

針對於UICollectionView,我們可以做以下處理:
預取和後臺執行
我們為預取的cell進行在後臺即全域性非同步佇列(global
asynchronous queues)的影象解碼,這是我們提到的兩種優化技術。

但是,在使用全域性非同步佇列的時候,可能會出現執行緒爆炸的問題:如果我們一次性處理多張影象,但是裝置只有2個CPU時,此時GCD會建立新的執行緒來處理解碼工作。建立新執行緒以及在不同執行緒之間切換是十分消耗時間和資源的。

解決執行緒爆炸問題

我們可以將解碼操作非同步的分派到序列佇列中,如下圖:
解決執行緒爆炸問題
這樣做可能使得某些影象的解碼過程延後,但是更多的是減少了線上程切換過程中浪費的時間和資源。

至此,我們完成了對於滾動檢視載入多張圖片的優化。

圖片資源的優化

接下來分析一下我們對於圖片資源的處理,在目前的程式中,可以展示的影象資源來源可能有一下幾種:

  1. 儲存在Image Assets中
  2. 儲存在Application或者Bundle的包中
  3. 儲存在沙盒的Document或者Cache檔案中
  4. 從網路下載的資料中

對於程式中自帶的影象資源,蘋果官方推薦我們使用Image Assets來儲存,以下是使用該方式的幾個優點:

  1. Image Assets針對基於名稱和特性的查詢進行了優化,它比在磁碟上搜索檔案要快。
  2. 在管理緩衝區方面也有優化。
  3. 允許對裝置安裝包進行優化,在下載App的時候會只下載能最高效果顯示在對應裝置的影象資源,從而減小安裝包大小。
  4. 對Vector Artwork的支援。

Vector Artwork

Vector Artwork是iOS 11引入的新特性。我們可以在Image Assets中勾選Preserve Vector Data來啟用它。如果啟用了Vector Artwork的話,當我們將影象顯示到一個比它原始尺寸大或者小的檢視中時,這個影象不會變的模糊。因為顯示的影象是從向量圖形中重新光柵化而來的,從而使得影象有清晰的邊緣。

啟用Vector Artwork後,影象的處理方式和之前圖片的處理方式類似,只是將解碼階段變為了光柵化階段,光柵化階段將向量資料轉換為點陣圖資料,進而供幀緩衝區讀取。

Vector Artwork讀取流程

如果我們把應用中所有的影象資源進行了Preserve Vector Data處理的話,會造成一些CPU的使用。
所以Xcode對這些情況做了一些處理:如果選中了Preserve Vector Data選項,但是在檢視上展示為原始尺寸大小時,Image Assets實際上已經完成了原始尺寸的光柵化,並把相關資料儲存在Image Assets中了,所以這種情況可以直接對儲存的資料進行解碼,而不進行光柵化。

另外一個建議是:如果計劃展示的尺寸為固定的幾個尺寸,那麼不要依賴Preserve Vector Data,而是準備好對應尺寸的影象資源,從而加快CPU對影象資源的處理。

自定義繪製內容

我們有時需要在程式中進行一些自定義的繪製,例如繪製一個如下樣式的檢視:
自定義控制元件

我們可以繼承UIView,然後在draw方法中進行相應的繪製:
自定義控制元件實現

但是我們並不推薦這麼使用,我們首先對比UIImageView和draw的實現原理來進行分析:我們都知道UIView是基於CALayer來顯示內容的;而對於UIImageView來說,UIImageView會將UIImage解碼後的資料交給CALayer來作為內容顯示;而對於draw來說,CALayer會建立一個與影象大小成正比的backing store來儲存影象資料,然後將影象資料拷貝至backing store中,然後將backing store中的內容繪製到幀緩衝區中。

由此可見,通過重寫draw方法可能會造成多餘的記憶體消耗以及資料的拷貝,接下來了解一下backing store

Backing Store

在我們重寫draw方法時,會觸發建立Backing Store,此時的Backing Store的大小與檢視的畫素大小成正比。

同時,在iOS12中,Backing Store將會使用動態增長的方式來減少記憶體的使用。在以前的iOS版本中,我們可以設定CALayer的contentsFromat屬性來指定Core Animation Framework在繪製時用到的顏色長度,這個設定會關閉iOS12中關於Backing Store的設定。

建議實現方式

迴歸主題,對於我們需要自定義繪製的檢視,我們應該減少Backing Store的使用。通常的做法是將一個大檢視構建為多個小檢視來實現;同時,系統提供的經過優化的檢視屬性也不會造成多餘記憶體的使用(例如UIView的backgroundColor屬性,就不會建立Backing Store。此時需要注意,使用pattern color時是一個例外,可以使用UIImageView作為UIView的子檢視來替代這一效果)。

將大檢視構建為多個小檢視

在Live檢視中,我們可以構建為以下層級:
Live檢視層級

1.圓角處理

在我們要進行圓角處理時,使用CALayer的cornerRadius屬性,該屬性不會引起多餘的記憶體使用。而對於使用maskView和maskLayer來說,會使用到額外的記憶體空間。

同時對於圓角之外的透明區域為複雜的不規則形狀且cornerRadius滿足不了需求時,可以使用UIImageView配合Resizeable的UIImage來進行處理。

2.Icon的實現

Icon可以使用UIImageView來顯示。

為了達到UIImage的複用,UIImageView支援對單色影象著色,同時可以直接渲染到幀緩衝區中。

這個設定需要對UIImage進行renderingMode的設定或者在Image Assets中進renderingMode設定。

最後,為UIImageView設定tintColor來完成最終顏色渲染。

3.UILabel的優化

  • UILabel對於顯示單色字串做了優化處理,可以節省75%大小的Backing Store的使用。

  • UILabel對於多彩字串以及emoji進行了動態增長Backing Store的優化。

最後,如果想離屏繪製,就是用UIGraphicsImageRenderer,而不是使用舊版本的UIGraphicsBeginImageContext。因為UIGraphicsImageRenderer支援寬顏色內容展示;同時也支援根據繪製的動作來動態的增加Image Buffer的大小。在iOS12中,UIGraphicsImageRendererFormatprefersExtendedRange屬性可以告知UIKit是否需要繪製寬顏色內容。同時,UIImage中提供了imageRendererFormat屬性來配合UIGraphicsImageRenderFormat來使用。

簡單講述在應用中使用先進的CPU和GPU技術。

Advanced Image Effects

  1. 實時處理影象時考慮Core Image。
  2. 儘量使用GPU來處理影象,進而解放CPU。
  3. 使用CIImage建立UIImage,並交由UIImageView去展示,這個過程會預設由GPU去處理,減少對CPU的使用。

Advanced Image Processing

  1. 使用CVPixelBuffer將資料移動到Metal, Vision和Accelerate等框架。
  2. 使用最好的初始值設定項(不要重複執行已經完成的工作)。
  3. 防止在GPU和CPU直接來回切換。
  4. 確保交給Accelerate的buffers是正確的格式。

總結

  1. 非同步的進行預處理。
  2. 使用UIImageView和UILabel來減少Backing Store的使用。
  3. 不要禁止自定義繪製時系統做的新優化。
  4. 使用Image Assets來儲存影象。
  5. 如果要展示相同圖片的不同尺寸,不要過多依賴Preserve Vector Data。