前言
在iOS開發中,保持介面流暢和良好的使用者體驗是非常重要的,那麼介面的優化對我們來說就是一個老生常談的話題,這裡總結了網上的一些資料和自己的一些開發經驗,講講介面效能的一些技巧.
UI卡頓、掉幀原因
借用網上一張圖來了解一下影象內容展示到螢幕的過程.最上面是VSync
垂直訊號,我們一般說頁面滑動的流暢是60FPS,指的就是每秒鐘會有60 幀的畫面更新,我們在人眼上看到的就是流暢的效果,那基於此呢,每個16.7ms(1/60s)就要產生一幀畫面,那麼在這16.7ms中就需要由CPU和GPU共同協同完成產生最終的一幀的資料並通過數模轉換傳遞給顯示器顯示.
其中CPU負責計算顯示內容,比如檢視的建立、佈局計算、圖片解碼、文字繪製等,隨後把產生的點陣圖提交給GPU,再由GPU進行圖層的合成、紋理渲染等.然後GPU將渲染結果提交到幀緩衝區去,等待下一次 VSync 訊號到來時顯示到螢幕上.而如果這個時候CPU或GPU處理的時間過長,當下一次VSync到來的時候,GPU還沒有將渲染結果提交到幀緩衝區中去,那麼這一幀就會被丟棄,畫面繼續保持之前的內容,等下下次VSync訊號到來的時候(如果此時GPU已將渲染結果提交到了幀緩衝區中),再顯示並重新整理介面.所以本來16.7ms後就該更新一幀畫面的,現在用了33.4ms(可能更多),這就是卡頓產生的原因.
那麼介面優化就主要從CPU和GPU兩個層面來優化了.
CPU
-
佈局計算、文字計算
檢視的佈局計算是介面優化的一個重點,一般對於檢視佈局的優化可以通過提前計算好檢視佈局和對檢視佈局進行快取.比如cell的高度、label的行高等涉及到計算的.對於比較複雜的介面,如果使用Autolayout
也會帶來效能上的問題,Masonry
這個庫就是基於Autolayout
.因為過多的約束帶來的計算量是非常大的,給CPU的消耗非常大.那麼這就容易造成CPU運算時間超時產生卡頓.如果用Frame
的話會好很多.這篇文章比較了Frame和Autolayout對效能的影響. 可以使用SDAutoLayout
這個庫,它的佈局是基於Frame的,或者使用 ComponentKit、AsyncDisplayKit 等框架.同樣Ulabel的寬高和繪製方法[NSAttributedString boundingRectWithSize:options:context:]
和 [NSAttributedString drawWithRect:options:context:]
也可以放在後臺執行緒進行以避免阻塞主執行緒.
-
物件建立
物件的建立會分配記憶體、調整屬性、甚至還有讀取檔案等操作,比較消耗 CPU 資源,所以將物件的建立放到子執行緒中去做會提高部分效能.通過 Storyboard 建立檢視物件還會涉及到檔案反序列化操作,比起程式碼建立的檢視,Storyboard要消耗更多的資源,所以對於一些對效能比較高的介面最好是通過程式碼來進行建立和佈局.如果沒有涉及到觸控事件等操作,可以用CALayer替代UIView,因為CALayer相對於UIView來說要輕量很多.
-
文字渲染
如上圖所示,螢幕上能看到的所有文字內容控制元件,包括 UIWebView,在底層都是通過 CoreText 排版、繪製為 Bitmap 顯示的。常見的文字控制元件 (UILabel、UITextView 等),其排版和繪製都是在主執行緒進行的,當顯示大量文字時,CPU 的壓力會非常大.所以可以自定義控制元件,直接使用 CoreText 進行排版控制,不過這樣太麻煩了,反正我不會這麼做,哈哈哈.
-
影象的繪製
影象的繪製是需要消耗CPU資源的,將繪製過程放在後臺執行緒中進行,然後再在主執行緒中將結果設定到layer的contents中,這樣會提高CPU的效率.程式碼如下:
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
複製程式碼
影象的繪製通常是指用那些以 CG 開頭的方法把影象繪製到畫布中,然後從畫布建立圖片並顯示的過程。前面的模組圖裡介紹了 CoreGraphic 是作用在 CPU 之上的,因此呼叫 CG 開頭的方法消耗的是 CPU 資源。我們可以將繪製過程放到後臺執行緒,然後在主執行緒裡將結果設定到 layer 的 contents 中.
GPU
相對於 CPU 來說,GPU 能幹的事情比較單一:主要也就是紋理(圖片)和形狀(三角模擬的向量圖形)兩類。
-
紋理的渲染
所有的 Bitmap,包括圖片、文字、柵格化的內容,最終都要由記憶體提交到視訊記憶體,繫結為 GPU Texture。不論是提交到視訊記憶體的過程,還是 GPU 調整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片並且快速滑動時),CPU 佔用率很低,GPU 佔用非常高,介面仍然會掉幀.
-
檢視的混合
當多個檢視(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果檢視結構過於複雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應用應當儘量減少檢視數量和層次,並在不透明的檢視裡標明 opaque 屬性以避免無用的 Alpha 通道合成。當然,這也可以用上面的方法,把多個檢視預先渲染為一張圖片來顯示.
-
離屏渲染
CALayer 的 border、圓角、陰影、遮罩(mask),CASharpLayer 的向量圖形顯示,通常會觸發離屏渲染(offscreen rendering),而離屏渲染通常發生在 GPU 中。當一個列表檢視中出現大量圓角的 CALayer,並且快速滑動時,可以觀察到 GPU 資源已經佔滿,而 CPU 資源消耗很少。這時介面仍然能正常滑動,但平均幀數會降到很低。為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉嫁到 CPU 上去。對於只需要圓角的某些場合,也可以用一張已經繪製好的圓角圖片覆蓋到原本檢視上面來模擬相同的視覺效果。最徹底的解決辦法,就是把需要顯示的圖形在後臺執行緒繪製為圖片.設定了以下屬性時,都會觸發離屏渲染:
-
layer.shouldRasterize
,光柵化 -
layer.mask
,遮罩 -
layer.allowsGroupOpacity
為YES,layer.opacity
的值小於1.0 -
layer.cornerRadius
,並且設定layer.masksToBounds
為YES
。可以使用剪下過的圖片,或者使用layer畫來解決 -
layer.shadows
,(表示相關的shadow開頭的屬性),使用shadowPath
代替