離屏渲染(Offscreen Render)
objc.io出品的Getting Pixels onto the Screen的翻譯版《繪製像素到螢幕上》應該是國內對離屏渲染這個概念推廣力度最大的一篇文章了。文章裡提到「直接將圖層合成到幀的緩衝區中(在螢幕上)比先建立螢幕外緩衝區,然後渲染到紋理中,最後將結果渲染到幀的緩衝區中要廉價很多。因為這其中涉及兩次昂貴的環境轉換(轉換環境到螢幕外緩衝區,然後轉換環境到框架緩衝區)。」觸發離屏渲染後這種轉換髮生在每一幀,在介面的滾動過程中如果有大量的離屏渲染髮生時會嚴重影響幀率。
蘋果官方公開的的資料裡關於離屏渲染的資訊最早是在 2011年的 WWDC, 在多個 session 裡都提到了盡量避免會觸發離屏渲染的效果,包括:mask, shadow, group opacity, edge antialiasing。
最初應該是從英文開發人員那裡傳開的:使用 Core Graphics 裡的繪製 API 也會觸發離屏渲染,比如重寫 drawRect:。為什麼幾年前會產生這樣的認識不得而知。在 WWDC 2011: Understanding UIKit Rendering 這個 session 裡示範了「Core Animation Instruments」裡使用「Color Offscreen-Renderd Yellow」選項來檢測離屏渲染,在 WWDC 2014: Advanced Graphics and Animations for iOS Apps 也專門示範了這個工具。
Core Animation Instruments Debug Options
Designing for iOS: Graphics & Performance這篇文章也提到了使用 Core Graphics API 會觸發離屏渲染,這引出了 Andy Matuschak,蘋果 iOS 4.1-8 時期 UIKit 群組成員 ,WWDC 2011: Understanding UIKit Rendering 主講人之一,對這個觀點的回複,主要意思是:「Core Graphics 的繪製 API 的確會觸發離屏渲染,但不是那種 GPU 的離屏渲染。使用 Core Graphics 繪製 API 是在 CPU 上執行,觸發的是 CPU 版本的離屏渲染。」
本文以「Color Offscreen-Renderd Yellow」為觸發離屏渲染的標準,除非還有這個標準無法檢測出來的引發離屏渲染的行為。那麼 Core Graphics API 是不會觸發離屏渲染的,比如重寫drawRect:,而除了以上四種效果會觸發離屏渲染,使用系統提供的圓角效果也會觸發離屏渲染,比如這樣:
1 2 |
view.layer.cornerRadius = 5 view.layer.masksToBounds = true |
圓角最佳化前段時間在微博上刷了好一陣,不想湊熱鬧,不過這個話題必須講一講。
開始之前,先鋪墊一點基礎的東西。
UIView 和 CALayer 的關係
The Relationship Between Layers and Views 的解釋很細緻但是太囉嗦,簡單來說,UIView 是對 CALayer 的一個封裝。
出自 WWDC 2012: iOS App Performance: Graphics and Animations
CALayer 負責顯示內容contents,UIView 為其提供內容,以及負責處理觸摸等事件,參與響應鏈。CALayer 的結構如下,出自 Layers Have Their Own Background and Border:
CALayer 構成
CALayer 有三個視覺元素,中間的contents屬性是這樣聲明的:var contents: AnyObject?,實際上它必須是一個CGImage才能顯示。
當使用let view = UIView(frame: CGRectMake(0, 0, 200, 200))產生一個視圖對象並添加到螢幕上時,從 CALayer 的結構可以知道,這個視圖的 layer 的三個視覺元素是這樣的:contents為空白,背景顏色為空白(透明色),前景框寬度為0的前景框,這個視圖從視覺上看什麼都看不到。CALayer 文檔第一句話就是:「The CALayer class manages image-based content and allows you to perform animations on that content.」UIView 的顯示內容很大程度上就是一張圖片(CGImage)。
UIImageView
既然直接對 CALayer 的contents屬性賦值一個CGImage便能顯示圖片,所以 UIImageView 就順利成章地誕生了。實際上 UIImage 就是對 CGImage(或者 CIImage) 的一個輕量封裝。記得我剛接觸 iOS 時,搞不懂這兩者的區別,有人這樣對我說過,沒想到出處是這裡:
出自 WWDC 2012: iOS App Performance: Graphics and Animations
UIKit 和 Core Graphics 架構的聯絡很緊密,UIKit 裡帶CG首碼屬性的類基本上是對應 Core Graphics 架構裡的對象的封裝,UIKit 裡的繪製功能也是 Core Graphics 繪製 API 的封裝。Drawing with Quartz and UIKit列舉了這些對應關係。介面的內容主要是映像和文字,文字是怎麼顯示的。也是使用 Core Graphics 架構繪製出來的。
接下來,正式開始本文的話題。
RoundedCorner
設定圓角:
1 |
view.layer.cornerRadius = 5 |
這行代碼做了什麼。文檔中cornerRadius屬性的說明:
Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to YES causes the content to be clipped to the rounded corners.
很明了,只對前景框和背景色起作用,再看 CALayer 的結構,如果contents有內容或者內容的背景不是透明的話,還需要把這部分弄個角出來,不然合成的結果還是沒有圓角,所以才要修改masksToBounds為true(在 UIView 上對應的屬性是clipsToBounds,在 IB 裡對應的設定是「Clip Subiews」選項)。前些日子很熱鬧的圓角最佳化文章中的2篇指出是修改masksToBounds為true而非修改cornerRadius才是觸發離屏渲染的原因,但如果以「Color Offscreen-Renderd Yellow」的特徵為標準的話,這兩個屬性單獨作用時都不是引發離屏渲染的原因,他倆合體(masksToBounds = true, cornerRadius>0)才是。
系統圓角需要裁剪 layer 中間的contents,這其中裁剪工作和離屏渲染對效能的影響哪個占的比重大。我對此有點疑問。雖然系統圓角下裁剪工作和離屏渲染無法拆分,但可以單獨測試出裁剪工作對效能的影響。我使用上面提到的某篇最佳化圓角的文章提供的 Demo 在快速滾動下得到的幀率如下,在此基礎上驗證測試:
基礎幀率
圖中括弧內的數量代表滾動時同屏下圓角效果的個數。同時測試了圓角半徑對效能的影響,兩者沒有關係,cornerRadius分別為0.1和10的時候無明顯差別。使用「Color Offscreen-Renderd Yellow」來檢測時,只有圓角部分才會有黃色特徵,因此在cornerRadius = 0.1的時候基本觀測不到,如果你對cornerRadius和masksToBounds合體才能觸發離屏渲染有疑問,對比幀率就知道了。
這個 Demo 裡的最佳化方案是重繪圓角,作者給出了他在 iPhone 6 上的測試結果,非常好。奇怪的是 Demo 裡沒有將繪製圓角的工作放到後台,文章裡沒有對此進行解釋,不過這個 Demo 在我服役多年的 iPad mini 1代(iOS 9.3.1)上的運行結果是無法讓人滿意的,顯然應該放在後台重繪再切換到主線程設定內容。做個對比測試,前台圓角:主線程繪製圓角(Demo 的最佳化方法),後台圓角:將原 Demo 的繪製操作放到後台線程然後切換到主線程,同屏圓角數量為24個,對比結果:
圓角對比
前台圓角的效能稍好於系統圓角,後台圓角的表現和無圓角持平。經過測試,masksToBounds=true和cornerRadius>0在單獨作用的時候對效能基本沒有影響(針對無圓角,前台圓角和後台圓角),且單獨作用下無法觀察到離屏渲染時的黃色特徵,也就是說只有系統圓角才觸發了離屏渲染。
對比上面的測試結果,眼看就要得出「在系統圓角中(阻塞主線程的)裁剪工作是影響效能的主要因素,黑鍋不該離屏渲染來背。」的結論來了。視圖效能出現問題時,要分清瓶頸是在 CPU 還是 GPU 上,使用 GPU Driver Instruments 來檢測。以下測試中同屏圓角數量在24個左右: