前言
在本系列文章的前兩篇中,我們已經了解了iOS Graphic相關的基本概念和影響Graphic性能的諸多因素和優化方案。在** “動畫平滑性優化” **一節中,當時指出了“離屏渲染(Offscreen Rendering)”這個概念。
離屏渲染,顧名思義,是指當前屏幕的繪制操作并不是直接發生在當前的幀緩沖區內,而是會合并/渲染圖層樹的一部分到一個新的緩沖區,然后該緩沖區被渲染到屏幕上。產生離屏渲染的原因有很多,它可以被 Core Animation 自動觸發,也可以被應用程序強制觸發。當圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制時,屏幕外渲染就被喚起了。屏幕外渲染并不意味著軟件繪制,但是它意味著圖層必須在被顯示之前在一個屏幕外上下文(Context)中被渲染(不論CPU還是GPU)。
一般情況下,你需要避免離屏渲染,因為這是很大的消耗。直接將圖層合成到幀的緩沖區中(在屏幕上)比先創建屏幕外緩沖區,然后渲染到紋理中,最后將結果渲染到幀的緩沖區中要廉價很多。因為這其中涉及兩次昂貴的環境轉換(轉換環境到屏幕外緩沖區,然后轉換環境到幀緩沖區)。
“狹義離屏渲染” vs. “廣義離屏渲染”
“離屏渲染”本身其實是GPU的概念,但是這個概念已經被大家廣泛討論,絕大多數情況下大家討論的已經是以中種更為廣泛的理解。但是從原理上來說,我們應該還是要搞清楚它們的區別。這點Apple UIKit team的Andy Matuschak在這里明確指出了。我在這里稍作整理:
廣義上:
CPU在后臺進行的bitmap生成。這部分(通過drawRect并且使用任何CoreGraphics來實現的繪制,或使用CoreText[其實就是使用CoreGraphics]繪制)的確涉及到了“離屏繪制”,但是這和我們通常說的那種離屏繪制是有區別的。當你實現drawRect方法或者通過CoeGraphics繪制的時候,其實你是在使用CPU繪制。并且這個繪制的過程會在你的App內同步地進行。基本上你只是調用了一些向位圖緩存內寫入一些二進制信息的方法而已。狹義上:
是真正指GPU的off-sceen rendering。這種形式離屏繪制是發生在繪制服務(是獨立的處理過程)并且同時通過GPU執行(這里就不是像前面提到的用CPU了)。當OpengGL的繪制程序在繪制每個layer的時候,有可能因為包含多子層級關系而必須停下來把他們合成到一個單獨的緩存里。你可能認為GPU應該總是比CPU牛逼一點,但是在這里我們還是需要慎重的考慮一下。因為對GPU來說,從當前屏幕(on-screen)到離屏(off-screen)上下文環境的來回切換(這個過程必須flush管線(pipeline)和光柵(rasterizing)),代價是非常大的。因此對一些簡單的繪制過程來說,這個過程有可能用CoreGraphics,全部用CPU來完成反而會比GPU做得更好。所以如果你正在嘗試處理一些復雜的層級,并且在猶豫到底用-[CALayer setShouldRasterize:]還是通過CoreGraphics來繪制層級上的所有內容,唯一的方法就是測試并且進行權衡。
離屏渲染的觸發
關于離屏渲染的觸發條件,目前已經有很多很好的文章做出了總結,各種示例也是層出不窮,后文的參考鏈接里我就放了幾篇很好的文章,這里就不再累述了,我只將基本條件提煉出來做一個簡單的列表:
離屏渲染觸發條件:
Core Graphics (any class prefixed with CG*) (CPU)
The drawRect() method, even with an empty implementation. (CPU)
CALayers with a shouldRasterize property set to YES.
CALayers using masks (setMasksToBounds) and round corner.
dynamic shadows (setShadow*).
Any text displayed on screen, including Core Text.
Group opacity (UIViewGroupOpacity).
edge antialiasing(抗鋸齒)
漸變
降低離屏渲染對性能的影響
離屏渲染合成計算是非常昂貴的, 但有時你也許希望強制這種操作。那么這時某些情況下,你是可以選擇不同的方式來減小性能的影響的:
陰影shadow
正如上一篇文章提到的,使用setShadowPath而不是直接使用shadow屬性,會大大提升CG server的渲染性能圓角round corner
事實上,單獨使用setMasksToBounds和單獨設置圓角并不會觸發離屏渲染,但是兩者結合一定會觸發離屏渲染。iOS 9.0 之前UIimageView跟UIButton設置圓角都會觸發離屏渲染;iOS 9.0 之后UIButton設置圓角會觸發離屏渲染,而UIImageView里png圖片設置圓角不會觸發離屏渲染了,如果設置其他陰影效果之類的還是會觸發離屏渲染的。有時候你想顯示圓角并沿著圖層裁切子圖層的時候,你可能會發現你并不需要沿著圓角裁切,這個情況下用CAShapeLayer就可以避免這個問題了。如果你想要的只是圓角且沿著矩形邊界裁切,同時還不希望引起性能問題。其實你可以用現成的UIBezierPath的構造器+bezierPathWithRoundedRect:cornerRadius:
這樣做并不會比直接用cornerRadius更快,但是它避免了性能問題。
//create shape layer CAShapeLayer *blueLayer = [CAShapeLayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.fillColor = [UIColor blueColor].CGColor;
blueLayer.path = [UIBezierPath bezierPathWithRoundedRect:
CGRectMake(0, 0, 100, 100) cornerRadius:20].CGPath; //add it to our view
[self.layerView.layer addSublayer:blueLayer];
- 光柵化 Rasterize
這里要解釋一下,使用光柵化確實會觸發離屏渲染,但是并不代表一定會造成性能下降,相反,如果你必須使用drawRect等API進行繪制時,如果Layerd的內容并不經常變化,使用光柵化讓CPU對繪制的內容進行bitmap緩存后反而能夠顯著提高性能;如果你的程序混合了很多圖層,并且想要他們一起做動畫,GPU 通常會為每一幀(1/60s)重復合成所有的圖層。當使用離屏渲染時,GPU 第一次會混合所有圖層到一個基于新的紋理的位圖緩存上,然后使用這個紋理來繪制到屏幕上。現在,當這些圖層一起移動的時候,GPU 便可以復用這個位圖緩存,并且只需要做很少的工作。需要注意的是,只有當那些圖層不改變時,這才可以用。如果那些圖層改變了,GPU 需要重新創建位圖緩存。比如在TableView和CollectionView中,對Cell的內容繪制完畢后進行光柵化,會使得完全處于屏幕中的Cell在滾動時會復用已經緩存的位圖而不會反復繪制。如果經常變化,那么設置 shouldRasterize 是不正確的,因為這時GPU會頻繁的更新緩存而讓空間換時間的trade off失去意義。這就是為什么在本系列上一篇文章(2)中,我們使用手動繪制bitmap然后在drawRect中填充,而不是用shouldRasterize的原因所在。
一個小Demo
不免俗的,我也提供一個小Demo,你可以在Github找到項目的源碼。
在這個Demo中,我分別針對Layer和View,使用了幾種不同的增加陰影的方式,并通過開關控制,你能夠看到幾個不同的屬性共同作用下的不同效果,尤其是圓角和陰影的配合使用方法。其次,我在一個swapView中使用了shouldRasterize,當然這是一個小Demo,所以你可能看不出這里面的性能的顯著變化,但是你仍然可以配合instrument看到不同的狀態下整個頁面中參與離屏渲染的范圍。
總結
至此,本系列文章告一段落。我希望它們能夠從基本概念開始指導你一步步的更深入的理解iOS UI 圖形性能,讓你在將來的工作中能夠少走一些彎路。當然水平有限,歡迎大家討論指正。
2016.8.16 完稿于南京
參考資料:
Mastering UIKit Performance
簡書 -《iOS 離屏渲染的研究》
簡書 -《深刻理解移動端優化之離屏渲染》
簡書 -《圓角繪制引發的離屏渲染》
When should I set layer.shouldRasterize to YES