本文授權轉載,作者:seedante(簡書)
離屏渲染(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 的基准性能:
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.不緩存,和系統圓角一樣,每次都重繪,浪費電量。
說了這麼多,重繪方案與其他的優化方案相比,並沒有什麼優勢。來看看其他方案:
如果不需要對外部來源的圖片做圓角,由設計師直接畫成圓角圖片是最方便的;
混合圖層:在要添加圓角的視圖上再疊加一個部分透明的視圖,只對圓角部分進行遮擋。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,因此陰影與系統圓角不兼容。但是注意,只是在視覺上看不到,對性能的影響依然。通常這樣實現一個陰影:
let imageViewLayer = avatorView.layer imageViewLayer.shadowColor = UIColor.blackColor().CGColor imageViewLayer.shadowOpacity = 1.0 //此參數默認為0,即陰影不顯示 imageViewLayer.shadowRadius = 2.0 //給陰影加上圓角,對性能無明顯影響 imageViewLayer.shadowOffset = CGSize(width: 5, height: 5) //設定路徑:與視圖的邊界相同 let path = UIBezierPath(rect: cell.imageView.bounds) imageViewLayer.shadowPath = path.CGPath//路徑默認為 nil
在 OffscreenRenderDemo 裡,僅開啟陰影(沒有指定路徑,同屏數量10個以上)在滾動時幀率會大幅下降,檢測到離屏渲染的黃色特征;指定一個與邊界相同的簡單路徑後離屏渲染特征消失,幀率恢復正常。
測試結果:
為陰影指定路徑前後 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 { let maskLayer = CALayer() maskLayer.frame = avatorView.bounds maskLayer.contents = maskImage?.CGImage avatorView.layer.mask = maskLayer }
如果所有 maskImage 相同的話,使用一個 maskImage 就夠了,不然每次生成一個新的 UIImage 也會是一個性能隱患點。注意:可以使用同一個 maskImage,但不能使用同一個 maskView,不然同時只會有一個 mask 效果。
測試結果:
maskImage 的透明面積是否影響性能?粗略測試,並無影響,至少在 Demo 裡 Size(80, 80) 這種級別的尺寸下沒有什麼明顯影響。
兩組測試的 CPU 利用率大部分時間都在50%以下,無明顯差別。看第1組數據,很有意思,在同屏 mask 數量為10的情況下,性能幾乎無影響,盡管此時 GPU 的利用率有點偏高,但是還能搞得定,保證了滾動的流暢;mask 數量增長到20後,GPU 使用率漲幅明顯,在 mask 數量為10的情況 GPU 的利用率已經偏高,數量增加10後,GPU 撐不住了,滾動幀率下降得很厲害。與前面兩種效果相比,mask 引發的離屏渲染對性能的影響弱一些。
Mask 效果無法取消離屏渲染,使用混合圖層的方法來模擬 mask 效果,性能各方面都是和無效果持平。
使用 mask 來實現圓角時也可以不用圖片,而使用 CAShapeLayer 來指定混合的路徑。
let roundedRectPath = UIBezierPath(roundedRect: avatorView.bounds, byRoundingCorners: .AllCorners, cornerRadii: CGSize(width: 10, height: 10)) let shapeLayer = 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 是指 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。開啟後與原來的測試對比:
以上 Rasterization 與 shadowPath 至少保留一個默認設置。與指定路徑相比,Rasterization 的 GPU 利用率要高一些。
從上面的數據來看,Rasterization 的優化效果是非常給力的。對於 GPU 而言,利用率在60%以下時,界面能夠維持較高的幀率。
前面提到如果視圖內容是動態變化的,使用 Rasterization 有可能讓性能變得更糟。什麼情況下會遇到動態內容的視圖呢,能想到的只有後台下載圖片完畢後切換到主線程設置這種了。來模擬下,在tableView:cellForRowAtIndexPath:裡調用以下方法:
func dynamicallyUpdateCell(cell: UITableViewCell){ let number = Int(UInt32(arc4random()) % UInt32(10)) let labelL = cell.viewWithTag(30) as! UILabel labelL.text = "OffscreenRender" + String(number) let avatorViewL = cell.viewWithTag(10) as! UIImageView avatorViewL.layer.cornerRadius = CGFloat(number) avatorViewL.clipsToBounds = true let delay = 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 不會難看,比預計的性能要好多了。
總結
RoundedCorner 在僅指定cornerRadius時不會觸發離屏渲染,僅適用於特殊情況:contents為 nil 或者contents不會遮擋背景色圓角;
Shawdow 可以通過指定路徑來取消離屏渲染;
Mask 無法取消離屏渲染;
以上效果在同等數量的規模下,對性能的影響等級:Shadow > RoundedCorner > Mask > GroupOpacity(迷之效果)。
任何時候優先考慮避免觸發離屏渲染,無法避免時優化方案有兩種:
Rasterization:適用於靜態內容的視圖,也就是內部結構和內容不發生變化的視圖,對上面的所有效果而言,在實現成本以及性能上最均衡的。即使是動態變化的視圖,開啟 Rasterization 後能夠有效降低 GPU 的負荷,不過在動態視圖裡是否啟用還是看 Instruments 的數據。
規避離屏渲染,用其他手法來模擬效果,混合圖層是個性能最好、耗能最少的通用優化方案,尤其對於 rounded corer 和 mask。
最後附上Demo: OffscreenRenderDemo。