作者: hi_xgb
地址: http://www.lxweimin.com/p/1b5cbf155b31
前言
在軟件開發領域里經常能聽到這樣一句話,“過早的優化是萬惡之源”,不要過早優化或者過度優化。我認為在編碼過程中時刻注意性能影響是有必要的,但凡事都有個度,不能為了性能耽誤了開發進度。在時間緊急的情況下我們往往采用“quick and dirty”的方案來快速出成果,后面再迭代優化,即所謂的敏捷開發。與之相對應的是傳統軟件開發中的瀑布流開發流程。
卡頓產生的原因
在 iOS 系統中,圖像內容展示到屏幕的過程需要 CPU 和 GPU 共同參與。CPU 負責計算顯示內容,比如視圖的創建、布局計算、圖片解碼、文本繪制等。隨后 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。之后 GPU 會把渲染結果提交到幀緩沖區去,等待下一次 VSync 信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。
因此,我們需要平衡 CPU 和 GPU 的負荷避免一方超負荷運算。為了做到這一點,我們首先得了解 CPU 和 GPU 各自負責哪些內容。
上面的圖展示了 iOS 系統下各個模塊所處的位置,下面我們再具體看一下 CPU 和 GPU 對應了哪些操作。
CPU 消耗型任務
布局計算
布局計算是 iOS 中最為常見的消耗 CPU 資源的地方,如果視圖層級關系比較復雜,計算出所有圖層的布局信息就會消耗一部分時間。因此我們應該盡量提前計算好布局信息,然后在合適的時機調整對應的屬性。還要避免不必要的更新,只在真正發生了布局改變時再更新。
對象創建
對象創建過程伴隨著內存分配、屬性設置、甚至還有讀取文件等操作,比較消耗 CPU 資源。盡量用輕量的對象代替重量的對象,可以對性能有所優化。比如 CALayer 比 UIView 要輕量許多,如果視圖元素不需要響應觸摸事件,用 CALayer 會更加合適。
通過 Storyboard 創建視圖對象還會涉及到文件反序列化操作,其資源消耗會比直接通過代碼創建對象要大非常多,在性能敏感的界面里,Storyboard 并不是一個好的技術選擇。
對于列表類型的頁面,還可以參考 UITableView 的復用機制。每次要初始化 View 對象時先根據 identifier 從緩存池里取,能取到就復用這個 View 對象,取不到再真正執行初始化過程。滑動屏幕時,會將滑出屏幕外的 View 對象根據 identifier 放入緩存池,新進入屏幕可見范圍內的 View 又根據前面的規則來決定是否要真正初始化。
Autolayout
Autolayout 是蘋果在 iOS6 之后新引入的布局技術,在大多數情況下這一技術都能大大提升開發速度,特別是在需要處理多語言時。比如阿拉伯語下布局是從右往左,通過 Autolayout 設置 leading 和 trailing 即可。
但是 Autolayout 對于復雜視圖來說常常會產生嚴重的性能問題,對于性能敏感的頁面建議還是使用手動布局的方式,并控制好刷新頻率,做到真正需要調整布局時再重新布局。
文本計算
如果一個界面中包含大量文本(比如微博、微信朋友圈等),文本的寬高計算會占用很大一部分資源,并且不可避免。
一個比較常見的場景是在 UITableView 中,heightForRowAtIndexPath
這個方法會被頻繁調用,即使不是耗時的計算在調用次數多了之后也會帶來性能損耗。這里的優化就是盡量避免每次都重新進行文本的行高計算,可以在獲取到 Model 數據后就根據文本內容計算好布局信息,然后將這份布局信息作為一個屬性保存到對應的 Model 中,這樣在 UITableView 的回調中就可以直接使用 Model 中的屬性,減少了文本的計算。
文本渲染
屏幕上能看到的所有文本內容控件,包括 UIWebView,在底層都是通過 CoreText 排版、繪制為 Bitmap 顯示的。常見的文本控件 (UILabel、UITextView 等),其排版和繪制都是在主線程進行的,當顯示大量文本時,CPU 的壓力會非常大。
這一部分的性能優化就需要我們放棄使用系統提供的上層控件轉而直接使用 CoreText 進行排版控制。
Wherever possible, try to avoid making changes to the frame of a view that contains text, because it will cause the text to be redrawn. For example, if you need to display a static block of text in the corner of a layer that frequently changes size, put the text in a sublayer instead.
上面這段話引用自 iOS Core Animation: Advanced Techniques,翻譯過來的意思就是說包含文本的視圖在改變布局時會觸發文本的重新渲染,對于靜態文本我們應該盡量減少它所在視圖的布局修改。
圖像的繪制
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中,然后從畫布創建圖片并顯示的過程。前面的模塊圖里介紹了 CoreGraphic 是作用在 CPU 之上的,因此調用 CG 開頭的方法消耗的是 CPU 資源。我們可以將繪制過程放到后臺線程,然后在主線程里將結果設置到 layer 的 contents 中。代碼如下:
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
圖片的解碼
Once an image file has been loaded, it must then be decompressed. This decompression can be a computationally complex task and take considerable time. The decompressed image will also use substantially more memory than the original.
圖片被加載后需要解碼,圖片的解碼是一個復雜耗時的過程,并且需要占用比原始圖片還多的內存資源。
為了節省內存,iOS 系統會延遲解碼過程, 在圖片被設置到 layer 的 contents 屬性或者設置成 UIImageView 的 image 屬性后才會執行解碼過程,但是這兩個操作都是在主線程進行,還是會帶來性能問題。
如果想要提前解碼,可以使用 ImageIO 或者提前將圖片繪制到 CGContext 中,這部分實踐可以參考 iOS Core Animation: Advanced Techniques( http://apprize.info/apple/ios_5/15.html )
這里多提一點,常用的 UIImage 加載方法有 imageNamed和 imageWithContentsOfFile。其中 imageNamed加載圖片后會馬上解碼,并且系統會將解碼后的圖片緩存起來,但是這個緩存策略是不公開的,我們無法知道圖片什么時候會被釋放。因此在一些性能敏感的頁面,我們還可以用 static 變量 hold 住 imageNamed加載到的圖片避免被釋放掉,以空間換時間的方式來提高性能。
GPU消耗型任務
相對于 CPU 來說,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合并渲染,然后輸出到屏幕上。寬泛的說,大多數 CALayer 的屬性都是用 GPU 來繪制。
以下一些操作會降低 GPU 繪制的性能,
大量幾何結構
所有的 Bitmap,包括圖片、文本、柵格化的內容,最終都要由內存提交到顯存,綁定為 GPU Texture。不論是提交到顯存的過程,還是 GPU 調整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片并且快速滑動時),CPU 占用率很低,GPU 占用非常高,界面仍然會掉幀。避免這種情況的方法只能是盡量減少在短時間內大量圖片的顯示,盡可能將多張圖片合成為一張進行顯示。
另外當圖片過大,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。
視圖的混合
當多個視圖(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果視圖結構過于復雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應用應當盡量減少視圖數量和層次,并且減少不必要的透明視圖。
離屏渲染
離屏渲染是指圖層在被顯示之前是在當前屏幕緩沖區以外開辟的一個緩沖區進行渲染操作。
離屏渲染需要多次切換上下文環境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上又需要將上下文環境從離屏切換到當前屏幕,而上下文環境的切換是一項高開銷的動作。
會造成 offscreen rendering 的原因有:
1.陰影(UIView.layer.shadowOffset/shadowRadius/…)
2.圓角(當 UIView.layer.cornerRadius 和 UIView.layer.maskToBounds 一起使用時)
3.圖層蒙板
4.開啟光柵化(shouldRasterize = true)
使用陰影時同時設置 shadowPath 就能避免離屏渲染大大提升性能,后面會有一個 Demo 來演示;圓角觸發的離屏渲染可以用 CoreGraphics 將圖片處理成圓角來避免。
CALayer 有一個 shouldRasterize 屬性,將這個屬性設置成 true 后就開啟了光柵化。開啟光柵化后會將圖層繪制到一個屏幕外的圖像,然后這個圖像將會被緩存起來并繪制到實際圖層的 contents 和子圖層,對于有很多的子圖層或者有復雜的效果應用,這樣做就會比重繪所有事務的所有幀來更加高效。但是光柵化原始圖像需要時間,而且會消耗額外的內存。
光柵化也會帶來一定的性能損耗,是否要開啟就要根據實際的使用場景了,圖層內容頻繁變化時不建議使用。最好還是用 Instruments 比對開啟前后的 FPS 來看是否起到了優化效果。
注意:shouldRasterize = true 時記得同時設置 rasterizationScale
Instruments 使用
Instruments 是一系列工具集,我們這里只演示 Core Animation 的使用。在 Core Animation 選項右下方會看到如下選項,
Color Blended Layers
這個選項選項基于渲染程度對屏幕中的混合區域進行綠到紅的高亮顯示,越紅表示性能越差,會對幀率等指標造成較大的影響。紅色通常是由于多個半透明圖層疊加引起。
Color Hits Green and Misses Red
當 UIView.layer.shouldRasterize = YES 時,耗時的圖片繪制會被緩存,并當做一個簡單的扁平圖片來呈現。這時候,如果頁面的其他區塊(比如 UITableViewCell 的復用)使用緩存直接命中,就顯示綠色,反之,如果不命中,這時就顯示紅色。紅色越多,性能越差。因為柵格化生成緩存的過程是有開銷的,如果緩存能被大量命中和有效使用,則總體上會降低開銷,反之則意味著要頻繁生成新的緩存,這會讓性能問題雪上加霜。
Color Copied Images
對于 GPU 不支持的色彩格式的圖片只能由 CPU 來處理,把這樣的圖片標為藍色。藍色越多,性能越差。
Color Immediately
通常 Core Animation Instruments 以每毫秒 10 次的頻率更新圖層調試顏色。對某些效果來說,這顯然太慢了。這個選項就可以用來設置每幀都更新(可能會影響到渲染性能,而且會導致幀率測量不準,所以不要一直都設置它)。
Color Misaligned Images
這個選項檢查了圖片是否被縮放,以及像素是否對齊。被放縮的圖片會被標記為黃色,像素不對齊則會標注為紫色。黃色、紫色越多,性能越差。
Color Offscreen-Rendered Yellow
這個選項會把那些離屏渲染的圖層顯示為黃色。黃色越多,性能越差。這些顯示為黃色的圖層很可能需要用 shadowPath 或者 shouldRasterize 來優化。
Color OpenGL Fast Path Blue
這個選項會把任何直接使用 OpenGL 繪制的圖層顯示為藍色。藍色越多,性能越好。如果僅僅使用 UIKit 或者 Core Animation 的 API,那么不會有任何效果。
Flash Updated Regions
這個選項會把重繪的內容顯示為黃色。不該出現的黃色越多,性能越差。通常我們希望只是更新的部分被標記完黃色。
演示
上述幾個選項中常用來檢測性能的是 Color Blended Layers、Offscreen-Rendered Yellow 和 Color Hits Green and Misses Red。下面我重點演示一下離屏渲染和光柵化的檢測,寫了一個簡單的 Demo 設置了陰影效果,代碼如下:
view.layer.shadowOffset = CGSizeMake(1, 1);
view.layer.shadowOpacity = 1.0;
view.layer.shadowRadius = 2.0;
view.layer.shadowColor = [UIColor blackColor].CGColor;
// view.layer.shadowPath = CGPathCreateWithRect(CGRectMake(0, 0, 50, 50), NULL);
shadowPath
沒有設置時用 Instruments 檢測 FPS 基本在 20 以下(iPhone6設備),設置了 shadowPath
后基本維持在 55 左右,性能提升十分明顯。
下面來看一下光柵化的檢測,代碼如下,
view.layer.shouldRasterize = YES;
view.layer.rasterizationScale = [UIScreen mainScreen].scale;
勾選 Color Hits Green and Misses Red 選項后顯示如下:
我們可以看到在靜止時緩存都生效了,在快速滑動時緩存基本不起作用,因此是否要開啟光柵化還是得根據具體場景,用 Instruments 檢測開啟前后的性能來決定。
總結
本文主要總結了性能調優的一些理論知識,后面還介紹了 Instruments 中 Core Animation 的一些性能檢測指標用法。性能優化最重要的是要使用工具來檢測而不是猜測,先查看是否有離屏渲染等問題,再用 Time Profiler 分析一下耗時的函數調用。修改后再用工具分析是否有改善,一步一步執行,小心仔細。
建議大家也實際動手分析一下自己的應用,加深一下印象,enjoy~
參考資料
http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/