離屏渲染優化詳解:實例示范+性能測試

sbicrgw 8年前發布 | 16K 次閱讀 性能測試 iOS開發 移動開發

離屏渲染(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: ,而除了以上四種效果會觸發離屏渲染,使用系統提供的圓角效果也會觸發離屏渲染,比如這樣:

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

設置圓角:

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個左右:

系統圓角

系統圓角: 幀率很低,CPU 利用率較低,GPU 利用率很高

前臺圓角

前臺圓角:幀率比上面稍好,不穩定,CPU 利用率起伏很大,高峰接近100%,低谷在20%以下,GPU 利用率很低

后臺圓角

后臺圓角:幀率非常好,CPU 利用率起伏非常大,高峰超過120%,低谷在10%以下,GPU 利用率很低

慘遭打臉!有點意外的是前臺圓角的 CPU 使用率和后臺圓角一樣起伏都很大。重繪圓角時,繪制工作是由 CPU 完成的,這可能成為性能的瓶頸,在系統圓角下 GPU 是瓶頸,由于無法將離屏渲染和我所謂的裁剪工作分開,之前試圖用自行繪制圓角妄圖證明系統圓角里裁剪圓角的工作是影響性能主因的對比測試是沒有意義的。

Mastering UIKit Performance 里介紹離屏渲染時也舉了圓角的例子,他給出的代碼并沒有在后臺繪制圓角,另一方面他表示繪制圓角的代碼只會執行一次(在實際使用時的確應該這樣設計,只繪制一次,后續直接使用重繪的結果),但從貼出來的代碼來看繪制代碼無法只執行一次(畢竟是 Demo,沒有優化這一點,實際上就變成了和系統圓角一樣,滾動的每一幀都在重繪),這樣一來就變成了在主線程進行手工繪制圓角,優化效率不高,而且從最后貼出的幀率截圖來看并沒有達到結論所說的那樣高幀率以及穩定性。由于這篇文章并沒有開發源代碼,無法探明其中的差異。他的測試硬件是 iPhone 4(iOS 7.1.1),而我的 iPad mini 1代與 iPhone 4相差兩年,上面的 Demo 里的測試硬件是 iPhone 6,又相差2年,考慮到硬件性能的差異,重繪圓角應該放到后臺才是最優解。

OffscreenRenderDemo

還有其他的幾個效果需要測試,所以還是要寫個 Demo 的: OffscreenRenderDemo ,里面包括本文涉及的所有效果演示以及優化方案。測試的 Demo 還是老一套,TableView 配合圖像和文本,長這樣,接下來的效果測試都主要集中在左側的兩個 UIImageView 上,尺寸都為(80, 80),cell 高度為100。

界面元素

測試環境為:

  • iPad mini 1st generation with iOS 9.3.1
  • Xcode 7.3 with Swift 2.2
  • OS X 10.11.4

在 Demo 里實現了圓角的優化,這個話題還沒有結束呢,上一節只是證明了界面滾動過程中大量的離屏渲染確實是幀率殺手。再放圖就特別占地方了,接下來就用表格來呈現數據,數據是我目測計算出來的,會有誤差,而且是單次測試,但是量級是沒有問題的。接下來的描述中:左右代表在某個值附近浮動,以下代表都接近某個值,很少有超過的,以上代表絕大部分在某個值以上,但超過幅度不大。

OffscreenRenderDemo 的基準性能:

同屏系統圓角數量 平均 FPS GPU 平均利用率 CPU 利用率
57以上 10%以下 20%~40%
10 44左右 80%以上 10%~50%
20 35左右 90%以下 10%~40%

CPU 的利用率很難用平均數值呈現,從上面也可以看到 CPU 的利用率是周期性的波動,這是這類 Demo 的特點,這導致很難對比兩次測試中的 CPU 利用率。上面的表格里標注的波動范圍僅能當作 CPU 是否是性能瓶頸的參考,而不能與其他測試進行對比。上面的圖里 CPU 的采樣間隔是1ms,FPS 和 GPU 的利用率的采樣間隔是1s,這些是默認值。如果你希望增大 CPU 采樣間隔時間來形成類似的柱狀圖,基本上沒有意義,這里的數據是累計利用率,稍不注意看到的都超過100%。觸發離屏渲染的效果的瓶頸主要是 GPU,CPU 的利用率偏低,當然,視圖性能跟 CPU 和 GPU 都有關,后面的效果會對 CPU 的利用率做出說明。

在我的 Demo 里,后臺繪制圓角自不必說和無任何效果下的性能非常接近,在主線程繪制圓角的性能只是略微下降。這與上一個 Demo 的相關情況相差很大,上面的結果顯示在同屏幕圓角數量24個的情況下,平均幀率勉強在40左右,修正為20個測試一次,平均幀率依然在40附近徘徊。我的 Demo 在主線程以及后臺線程繪制圓角時 CPU 的利用率也不像上一個 Demo 那樣變化劇烈。由于代碼的差異,這些情況很難說明什么,但再次證明一點,為了高幀率,后臺繪制才是最優解。

大部分賺星星的方案都采用了重繪圓角,重繪的方式有多種,都是殊途同歸。實際中重繪圓角的優化方案需要考慮的是,將圖像重新繪制為為圓角圖像相當于多了一份拷貝,要不要緩存?A.第一次重繪后將這些圓角圖像緩存在磁盤里,第二次加載直接使用緩存的圓角圖像;B.直接保存在內存里,在內存比較吃緊時顯然不是個好選擇;C.不緩存,和系統圓角一樣,每次都重繪,浪費電量。

說了這么多,重繪方案與其他的優化方案相比,并沒有什么優勢。來看看其他方案:

  1. 如果不需要對外部來源的圖片做圓角,由設計師直接畫成圓角圖片是最方便的;
  2. 混合圖層:在要添加圓角的視圖上再疊加一個部分透明的視圖,只對圓角部分進行遮擋。 VVebo 微博客戶端就是這樣做的,遮擋的部分背景最好與周圍背景相同。多一個圖層會增加合成的工作量,但這點工作量與離屏渲染相比微不足道,性能上無論各方面都和無效果持平。下面左側的圖像是 VVebo 里用來制造圓形頭像的 mask 圖像,實際中有這種需求的基本是制造圓形頭像,普通的圓角遮罩需要左二這種,左三是通用型。如果疊加的視圖都一樣,可以只加載一次遮罩圖片以減少內存占用。

遮罩

除了用軟件畫出來保存在項目里,直接用代碼畫出來也是很簡單的。即使不熟悉 Core Graphics 的 API,搜索出來的重繪圓角的代碼看懂是很容易的,但要繪制出上面的圖形還是有點棘手。這種事情多試試就好了:在一個設置 opaque = false 的 CGContext 里,設定填充顏色然后用兩條貝塞爾曲線圍成一個封閉區域,最后從這個繪制環境導出圖像即可。我寫了個函數來生成區域圓角遮罩圖像: Draw a transparent image

如何在文本視圖類上實現圓角?文本視圖主要是這三類:UILabel, UITextField, UITextView。其中 UITextField 類自帶圓角風格的外型,UILabel 和 UITextView 要想顯示圓角需要表現出與周圍不同的背景色才行。想要在 UILabel 和 UITextView 上實現低成本的圓角(不觸發離屏渲染),需要保證 layer 的 contents 呈現透明的背景色,文本視圖類的 layer 的 contents 默認是透明的(字符就在這個透明的環境里繪制、顯示),此時只需要設置 layer 的 backgroundColor ,再加上 cornerRadius 就可以搞定了。不過 UILabel 上設置 backgroundColor 的行為被更改了,不再是設定 layer 的背景色而是為 contents 設置背景色,UITextView 則沒有改變這一點,所以在 UILabel 上實現圓角要這么做:

//不要這么做:label.backgroundColor = aColor 以及不要在 IB 里為 label 設置背景色
label.layer.backgroundColor = aColor
label.layer.cornerRadius = 5

Shadow

Shadow Properties 展示了陰影是如何與視圖本身結合的:

Layer displaying the shadow properties

陰影直接合成在視圖的下面,視圖結構里并沒有多出一個視圖。在沒有指定陰影路徑時,陰影是沿著視圖的非透明部分擴展的,而且 CALayer 的三個視覺元素至少有一個存在時才會有陰影。

使用陰影必須保證 layer 的 masksToBounds = false ,因此陰影與系統圓角不兼容。但是注意,只是在視覺上看不到,對性能的影響依然。通常這樣實現一個陰影:

letimageViewLayer = avatorView.layer
imageViewLayer.shadowColor = UIColor.blackColor().CGColor
imageViewLayer.shadowOpacity = 1.0 //此參數默認為0,即陰影不顯示
imageViewLayer.shadowRadius = 2.0 //給陰影加上圓角,對性能無明顯影響
imageViewLayer.shadowOffset = CGSize(width: 5, height: 5)
//設定路徑:與視圖的邊界相同
letpath = UIBezierPath(rect: cell.imageView.bounds)
imageViewLayer.shadowPath = path.CGPath//路徑默認為 nil

在 OffscreenRenderDemo 里,僅開啟陰影(沒有指定路徑,同屏數量10個以上)在滾動時幀率會大幅下降,檢測到離屏渲染的黃色特征;指定一個與邊界相同的簡單路徑后離屏渲染特征消失,幀率恢復正常。

測試結果:

條件 同屏 SHADOW 數量 平均 FPS GPU 平均利用率 離屏渲染特征
shadowPath = nil 10 38左右 73%左右
shadowPath != nil 10 56以上 15%以下
shadowPath = nil 20 22左右 80%左右
shadowPath != nil 20 56以上 20%以下

為陰影指定路徑前后 CPU 的利用率無明顯變化,大部分時間都在50%以下,無法判斷設定路徑是否增加了 CPU 的負擔。這里要吐槽下 CALayer 的設計, shadowPath 默認值為 nil,然而效果是與當視圖邊界路徑一致,如果 CALayer 默認添加與邊界相同的路徑完全可以避免這個問題。

除了指定路徑,實現良好性能陰影的方法還有:用圓角優化里混合圖層的方法模擬陰影的效果:放一個同樣效果的視圖在要添加陰影程度的視圖的下方;使用 Core Graphics 繪制陰影,不過除非萬不得已沒人想碰 Core Graphics API。從實現成本來講,都不如指定路徑方便。這兩種方法實現簡單形狀的陰影比較方便,比如圖中左側和中間的效果,面對右側的陰影效果就不好弄了,用指定路徑的方法實現也比較麻煩,還好,有更簡單方便的優化方法,看壓軸章節。

Mask

Mask 效果與混合圖層的效果非常相似,只是使用同一個遮罩圖像時,mask 與混合圖層的效果是相反的,在 Demo 里使用反向內容的遮罩來實現圓角。實現 mask 效果使用 CALayer 的 layer 屬性,在 iOS 8 以上可以使用 UIView 的 maskView 屬性。代碼:

if #available(iOS 8.0, *) {
    avatorView.maskView = UIImageView(image: maskImage)
} else {
    letmaskLayer = CALayer()
    maskLayer.frame = avatorView.bounds
    maskLayer.contents = maskImage?.CGImage
    avatorView.layer.mask = maskLayer            
}

如果所有 maskImage 相同的話,使用一個 maskImage 就夠了,不然每次生成一個新的 UIImage 也會是一個性能隱患點。注意:可以使用同一個 maskImage,但不能使用同一個 maskView,不然同時只會有一個 mask 效果。

測試結果:

同屏 MASK 數量 平均 FPS GPU 平均利用率 離屏渲染特征
10 55左右 60%左右
20 37左右 75%左右

maskImage 的透明面積是否影響性能?粗略測試,并無影響,至少在 Demo 里 Size(80, 80) 這種級別的尺寸下沒有什么明顯影響。

兩組測試的 CPU 利用率大部分時間都在50%以下,無明顯差別。看第1組數據,很有意思,在同屏 mask 數量為10的情況下,性能幾乎無影響,盡管此時 GPU 的利用率有點偏高,但是還能搞得定,保證了滾動的流暢;mask 數量增長到20后,GPU 使用率漲幅明顯,在 mask 數量為10的情況 GPU 的利用率已經偏高,數量增加10后,GPU 撐不住了,滾動幀率下降得很厲害。與前面兩種效果相比,mask 引發的離屏渲染對性能的影響弱一些。

Mask 效果無法取消離屏渲染,使用混合圖層的方法來模擬 mask 效果,性能各方面都是和無效果持平。

使用 mask 來實現圓角時也可以不用圖片,而使用 CAShapeLayer 來指定混合的路徑。

letroundedRectPath = UIBezierPath(roundedRect: avatorView.bounds, byRoundingCorners: .AllCorners, cornerRadii: CGSize(width: 10, height: 10))
letshapeLayer = CAShapeLayer()
shapeLayer.path = roundedRectPath.CGPath
avatorView.layer.mask = shapeLayer

同樣的 mask 效果使用 CAShapeLayer 時相比直接使用 maskImage 在幀率上稍低,CPU 利用率無明顯變化,但是 GPU 利用率也低一些。

WWDC 2014: Advanced Graphics and Animations for iOS Apps 里詳細講解了 mask 效果的渲染過程,老實說看上去和合成兩個視圖差不了多少,不過沒有更多的細節不知道兩者性能的差別在哪里。而且按照這個 session 的說法,系統圓角使用 mask 的方式實現的,不過顯然沒有優化好。另外這個 session 里 GPU Driver 還叫 Open GL ES Driver。

GroupOpacity

首先來看看 GroupOpacity 是什么效果:

GroupOpacity Sample.png

GroupOpacity 是指 CALayer 的 allowsGroupOpacity 屬性,UIView 的 alpha 屬性等同于 CALayer opacity 屬性。開啟 GroupOpacity 后,子 layer 在視覺上的透明度的上限是其父 layer 的 opacity 。

這個屬性的文檔說明:

The default value is read from the boolean UIViewGroupOpacity property in the main bundle’s Info.plist file. If no value is found, the default value is YES for apps linked against the iOS 7 SDK or later and NO for apps linked against an earlier SDK.

從 iOS 7 以后默認全局開啟了這個功能,這樣做是為了讓子視圖與其容器視圖保持同樣的透明度。

GroupOpacity 開啟離屏渲染的條件是: layer.opacity != 1.0 并且有子 layer 或者背景圖。

這個觸發條件并不需要 subLayer.opacity != 1.0 ,非常容易滿足。然而在 TableView 這樣的視圖里設置 cell 或 cell.contentView 的 alpha 屬性小于1并不能檢測離屏渲染的黃色特征,性能上也沒有明顯差別。經過摸索發現:只有設置 tableView 的 alpha 小于1時才會觸發離屏渲染,對性能無明顯影響;設置 cell 的 alpha 屬性并不會對整體的透明度產生影響,只有設置 cell.contentView 才有效。

在一般的 UIViewController 的視圖下可以很容易地觀察到 GroupOpacity 觸發的離屏渲染,這里只能猜測 TableView 更改了這些行為。

EdgeAntialiasing

經過測試,開啟 edge antialiasing(旋轉視圖并且設置 layer.allowsEdgeAntialiasing = true ) 在 iOS 8 和 iOS 9 上并不會觸發離屏渲染,對性能也沒有什么影響,也許到現在這個功能已經被優化了。

終極優化方案

除了 GroupOpacity 和 EdgeAntialiasing,其他效果觸發的離屏渲染都會對性能產生嚴重影響,離屏渲染真的是一無是處嗎?不,離屏渲染本來是個優化設計。如何物盡其用?答案是:Rasterization。在 OffscreenRenderDemo 里,只需要這么做:

cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = cell.layer.contentsScale

shouldRasterize = false 時,離屏渲染的黃色特征僅限于上述自動觸發離屏渲染的效果的部分, shouldRasterize = true 后該部分和開啟了該屬性的 layer 整體(在這里就是 cell 整體)都有黃色特征,所以開啟 Rasterization 是手動啟動了離屏渲染。

從前面來看,離屏渲染會給 GPU 帶來沉重的負擔,強制啟動豈不是更糟?開啟 Rasterization 后,GPU 只合成一次內容,然后復用合成的結果;合成的內容超過 100ms 沒有使用會從緩存里移除,在更新內容時還會產生更多的離屏渲染。對于內容不發生變化的視圖,原本拖后腿的離屏渲染就成為了助力;如果視圖內容是動態變化的,使用這個方案有可能讓性能變得更糟。

Core Animation Instruments 有個「Color Hits Green and Misses Red」的選項,開啟 Rasterization 后開啟這個選項,屏幕上綠色的部分表示有渲染緩存可用,紅色的部分表示無渲染緩存可用。在 OffscreenRenderDemo 里,針對以上任何一個效果開啟 Rasterization 后,滾動時還在屏幕范圍內的視圖會復用緩存的渲染結果,可以看到這部分被標記為綠色,即將出現在屏幕上,處于滾動邊緣范圍的視圖被標記為紅色。

默認情況下, shouldRasterize 屬性為 false 。開啟后與原來的測試對比:

條件 同屏系統圓角數量 平均 FPS GPU 平均利用率 離屏渲染特征
shouldRasterize = false 10 44左右 80%以上
shouldRasterize = true 10 55以上 20%以下
shouldRasterize = false 20 35左右 90%以下
shouldRasterize = true 20 55左右 20%左右
條件 同屏 SHADOW 數量 平均 FPS GPU 平均利用率 離屏渲染特征
shouldRasterize = false 10 38左右 73%左右
shouldRasterize = true 10 55以上 30%以下
shadowPath != nil 10 56以上 15%以下
shouldRasterize = false 20 22左右 80%左右
shouldRasterize = true 20 55左右 40%以下
shadowPath != nil 20 56以上 20%以下

以上 Rasterization 與 shadowPath 至少保留一個默認設置。與指定路徑相比,Rasterization 的 GPU 利用率要高一些。

條件 同屏 MASK 數量 平均 FPS GPU 平均利用率 離屏渲染特征
shouldRasterize = false 10 55左右 60%左右
shouldRasterize = true 10 55以上 20%左右
shouldRasterize = false 20 37左右 75%左右
shouldRasterize = true 20 55左右 30%以下

從上面的數據來看,Rasterization 的優化效果是非常給力的。對于 GPU 而言,利用率在60%以下時,界面能夠維持較高的幀率。

前面提到如果視圖內容是動態變化的,使用 Rasterization 有可能讓性能變得更糟。什么情況下會遇到動態內容的視圖呢,能想到的只有后臺下載圖片完畢后切換到主線程設置這種了。來模擬下,在 tableView:cellForRowAtIndexPath: 里調用以下方法:

funcdynamicallyUpdateCell(cell: UITableViewCell){
 
    letnumber = Int(UInt32(arc4random()) % UInt32(10))
 
    letlabelL = cell.viewWithTag(30) as! UILabel
    labelL.text = "OffscreenRender" + String(number)      
 
    letavatorViewL = cell.viewWithTag(10) as! UIImageView
    avatorViewL.layer.cornerRadius = CGFloat(number)
    avatorViewL.clipsToBounds = true
 
    letdelay = NSTimeInterval(number) * 0.1
    performSelector(#selector(TableViewController.dynamicallyUpdateCell(_:)), withObject: cell, afterDelay: delay)
}

這段代碼隨機時間內更新 UILabel 的內容和頭像圓角半徑,這里只設置了一半的視圖。下面是開啟 Rasterization 后同時設置兩個頭像和兩個 label 的性能,這里 GPU 的高峰在50%左右,CPU 的高峰接近100%,FPS 的高峰在55左右,低谷為20左右。

動態視圖

應用啟動后前8秒無操作;8~20秒滾動視圖;20~32秒無操作;32~42秒滾動視圖;42~56秒無操作;00:56~01:12滾動視圖;01:12~結束無操作。這里除了20~32秒 FPS 有點反常地高,其他都比較有規律:在無操作時 FPS 很低,在20左右,CPU 滿載,GPU 利用率也在高峰,大約50%;視圖滾動時 FPS 很高,在50以上,CPU 和 GPU 的利用率都有下降。

還需要了解的信息是:主線程繁忙的時候 performSelector:withObject:afterDelay: 會延后執行,所以在發生觸摸或是視圖還在滾動時這個方法不會運行;用「Color Hits Green and Misses Red」觀察離屏渲染對緩存的使用發現:GPU 能夠使用視圖部分內容的緩存,而不是每次更新都要重新渲染整個視圖,提升了渲染的效率。所以在視圖沒有滾動時并且 dynamicallyUpdateCell: 還在不停調用自身時可以看到畫面是紅綠斑駁的。

根據以上兩段信息來分析性能走勢:應用啟動后前8秒 CPU 的走勢還是挺隨機的,間歇性地達到較高的占用率,這一階段 CPU 是性能瓶頸,FPS 很低。視圖滾動時,由于 performSelector 不會執行,和普通的 tableView:cellForRowAtIndexPath: 方法調用并無二致,CPU 的利用率不高,在 Rasterization 的作用下,GPU 的利用率也不高,FPS 大幅提升;而視圖停止滾動后,performSelector 開始執行,似乎累計到一起的工作讓刻意設置的隨機性失去了作用,CPU 時刻滿載,GPU 的利用率也隨之提升,而得益于 Rasterization,并沒有到很高的地步,但由于 CPU 的滿載,FPS 降到很低。

從結果來看,開啟 Rasterization 后 GPU 的利用率始終不高,如果 CPU 的利用率控制得當的話 FPS 不會難看,比預計的性能要好多了。

總結

  1. RoundedCorner 在僅指定 cornerRadius 時不會觸發離屏渲染,僅適用于特殊情況: contents 為 nil 或者 contents 不會遮擋背景色圓角;
  2. Shawdow 可以通過指定路徑來取消離屏渲染;
  3. Mask 無法取消離屏渲染;

以上效果在同等數量的規模下,對性能的影響等級:Shadow > RoundedCorner > Mask > GroupOpacity(迷之效果)。

任何時候優先考慮避免觸發離屏渲染,無法避免時優化方案有兩種:

  1. Rasterization:適用于靜態內容的視圖,也就是內部結構和內容不發生變化的視圖,對上面的所有效果而言,在實現成本以及性能上最均衡的。即使是動態變化的視圖,開啟 Rasterization 后能夠有效降低 GPU 的負荷,不過在動態視圖里是否啟用還是看 Instruments 的數據。
  2. 規避離屏渲染,用其他手法來模擬效果,混合圖層是個性能最好、耗能最少的通用優化方案,尤其對于 rounded corer 和 mask。

 

 

來自:http://ios.jobbole.com/88618/

 

 本文由用戶 sbicrgw 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!