產生卡頓的原因
屏幕顯示圖像的原理
在通產情況下.計算機中的CPU和GPU,以及顯示器是以上面的這種方式來進行工作的.CPU計算好顯示內容,然后將這些內容提交到GPU當中,GPU經過渲染完成后將渲染的結構放入幀緩沖區,隨后視頻控制器會按照VSync信號來進行讀取幀緩沖區的數據,經過可能的數模轉換傳遞給顯示器顯示.
在最簡單的情況下,幀緩沖區只有一個,這時幀緩沖區的讀取和刷新都會有比較到的效率問題.為了解決效率問題,顯示系統通常會引入兩個緩沖區,即雙緩沖機制.在這種情況情況下,GPU會預先渲染好一幀放入一個緩沖區內,讓視頻控制器讀取,當下一幀渲染好后,GPU會之間把視頻控制器的指針直線第二個緩沖期內.如此一來效率會有很大的提升.
雙緩沖雖然能夠解決效率的問題,但會引入一個新的問題.當視頻控制器還未讀取完成時,即屏幕內容剛顯示一半時,GPU將新的一幀內容提交到幀緩沖區并把兩個緩沖區進行交換后,視頻控制器就會把新的一幀數據的下半段顯示到屏幕上,造成畫面撕裂現象,如:
為了解決這個問題,GPU通常有一個機制叫做垂直同步(簡寫也是V-Sync),當開啟垂直同步后,GPU會等待顯示器的VSync信號發出后,才進行新的一幀渲染和緩沖區更新.這樣能解決換面撕裂現象,也增加了換面流暢度,但需求消費更多的計算資源,也會帶來部分延遲.
在目前的主流的移動設備中出現的是什么情況?iOS的設備中會始終使用雙緩存,并開啟垂直同步.而安卓設備知道4.1版本后,Google才開始引用這種機制,目前安卓系統是三緩存+垂直同步.
解決方案
在VSync信號到來后,系統圖形服務會通過 CADisplayLink 等機制通知APP,APP主線程開始在CPU 中計算顯示內容,比如視圖的創建,布局計算,圖片解碼,文本繪制等.隨后CPU 會將計算好的內容提交到GPU上去,再由GPU進行變換,合成,渲染.隨后GPU 會把渲染的結果提價到幀緩沖區中,等待下一次VSync 信號到來顯示到屏幕上.由于垂直同步的機制,如果在一個VSync時間內,CPU或者是GPU沒有完成內容的提交,則那一幀就會被丟棄,等待下一次機會在顯示,而這時顯示屏幕就會保留之前的內容不變.這就是界面卡頓的現象.
從圖中可以看到,CPU和GPU不論哪個阻礙了顯示流程,都會在成掉幀現象.所以開發時,也需要分別對CPU和GPU壓力進行評估和優化.
CPU 資源消耗原因和解決方案
- 對象創建
對象的創建會分配內存,調整屬性,甚至還有讀取文件等操作,比較消耗CPU資源.盡量用輕量級的對象代替重量級的對象,可以對性能有所優化.比如在使用CALayer 的時候就比 UIView 要輕量許多,那么不需要響應觸摸時間的控件,用CALayer 顯示會更加合適.如果對象不涉及到UI操作,則盡量放到后臺線程去創建,但可惜的是包含有CALayer的控件,都只能在主線程創建和操作.通過Storyboard創建視圖對象時,其資源消耗的也會比直接通過代碼創建對象要大非常多,在性能敏感的界面里,Storyboard 并不是一個好的技術選擇.
盡量推遲對象創建的時間,并把對象的創建分散到多個任務中去.盡管這是吸納起來比較麻煩,并且帶來的有時并不多,但如果有能力去做,還是要嘗試一下.如果對象可是復用,并且復用的代價比釋放,創建新對象要小,那么這類對象應當放到一個緩存池復用.
-
對象調整
對象的調整也經常消耗CPU資源的地方.CALayaer:CALayer內部并沒有屬性,當調整屬性方法時,它內部是通過運行時resolveInstanceMethod
為對象臨時添加一個方法,并把對應屬性值保存到內部的一個Dictionary
里,同時還會通知Delegate ,創建動畫等等.這是非常消耗資源的 .UIView 的關于顯示相關的屬性(比如frame
/bounds
/transform
)等實際上都是CALayer屬性映射出來的,所以對UIView的這些屬性進行調整時,消耗的資源要遠大于一般的屬性.對此在應用中,應該盡量減少不必要的屬性修改.
當視圖層次調整時,UIView,CALayer 之間會出現很多的方法調整與通知,所以在優化性能時,應該盡量避免調整視圖層次,添加和移除視圖.
-
對象銷毀
對象的銷毀雖然消耗的資源不多,但是一旦積累起來也是不容易進行忽視的.通常當容器類持有大量的對象時,在進行銷毀時所累積的資源消耗也會非常大.同樣的,如果對象可以放到后臺線程中去釋放,那就挪到后臺線程去.這里有個小的Tip: 把對象捕獲到block中,然后再扔到后臺隊列去隨便發個消息以避免編輯器警告,就可以讓對象下后臺線程銷毀了.
NSArray *temp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[temp class];
});
-
布局計算
視圖布局的計算是APP中最為常見的消耗CPU資源的地方.如果能在后臺線程提前計算好視圖布局,并且對視圖布局進行緩存,那么這個地方基本就不會產生性能問題了.
不論通過何種技術對視圖進行布局,最終都會落到對 UIView.frame/bouonds/center
等屬性的調整上.對這些非常消耗資源的屬性,要盡量提前計算好布局,只在需要時一次性調整好對應的屬性,而不要多次,頻繁的計算和調整這些屬性.
- AUTOLAHYOUT
Autolayout
是蘋果本身提倡的技術,在大部分情況下也會提升開發效率,但所出現的問題,就是Autolayout
對于復雜視圖來說常常會產生嚴重的性能為題,并且隨著視圖的增加,這些問題也會逐漸進行積累.所以隨著Autolayout
帶來的CPU消耗,也會呈現指數的上升.可以通過(比如常見的 left / right / top / bottom / width / height
)這些屬性來代替,或是使用框架來代替.
- 文本計算
如果一個界面中包含了大量的文本,文本的寬高計算會占用很大的資源消耗,這些也都是不可避免的.如果沒有特殊的要求,可以使用UIlabel 內部的實現方式,用[NSAttributedString boundingRectWithSize:options:context:]
來進行文本寬高,用-[NSAttributedString drawWithRect:options:context:]
來繪制文本.盡管這兩個方法性能不錯,但是仍舊需要放到后臺線程進行以避免阻塞主線程.
如果使用CoreText繪制文本,那就可以先生成CoreText排版對象,然后自己計算,并且在使用CoreText對象還能保留以供以后繪制再次使用.
文本渲染
屏幕上能夠看到的所有文本內容控件,包括UIWebView
,在底層都是通過CoreText 來進行排版繪制,然后在Bitmap進行顯示.常見的文本控件(CoreText, UITextView)
等.但是這些控件排版和繪制都是在主線程進行的,當顯示大量文本時,CPU的壓力就會非常大.對此解決方案只有一個,那就是使用自定義文本控件,用TextKit
或者是最底層的CoreText
對文本進行繪制.盡管看起來這會非常的麻煩,但其所帶來的優勢也是非常大的,CoreText
對象創建好后,能直接獲取文本的寬高等信息,避免了多次計算(調整UILabel大小時再計算一遍,UIlabel 繪制時內部在進行計算); CoreText 對象占用內存較少,可以緩存先來以備稍后多次渲染.圖片的解碼
當在使用UIImage
或CGImageSource
的方法去創建圖片時,圖片數據并不會理解進行解碼.圖片設置到UIImageView
或者CALayer.contents
中去,并且CALayer 被提交到GPU 前,CGImage
中的數據才會得到解碼.這一步是發生在主線程的,不可避免.如果想要繞開這個機制,常見的做法就是在后臺線程先把這個繪制到CGBitmapContext中,然后在從Bitmap中直接創建圖片.目前常見的網絡圖片庫都自帶這個功能.圖像的繪制
圖像的繪制通常是指那些以CG開頭的方法把圖形繪制到畫布中,然后從畫布創建圖片并顯示這樣的一個過程.這個最常見的地方就是[UIView drawRect:]
里面了.由于CoreGraphic 方法通常都是線程安全的,所以圖像的繪制可以很容易的放到后臺線程進行.一個簡單異步繪制的過程大致如下.
- (void)display{
dispatch_async(backgroundQueue, ^{
CGontextRef ctx = CGBitmapContextCreate(....);
//draw in context....
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
GPU資源消耗
相對于CPU來說,GPU能干的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform),混合并渲染,然后輸出到屏幕上.通常就是所能看到的內容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類.
-
紋理的渲染
所有的Bitmap,包括圖片,文本,柵格化的內容,最終都要由內存提交到顯存.綁定為GPU Texture.不論是提交到顯存的過程,還是GPU 調整和渲染Texture的過程,都要消耗不少GPU 資源.當在較短時間顯示大量圖片的時候(比如用UICollectionView 來顯示圖片,并且上下進行滑動的時候),CPU占用率要比GPU的占用率要低,界面仍然會出現掉幀的現象.避免這種情況的發生只能是盡量減少在短時間內大量圖片的顯示,盡可能將多張圖片合成為一張進行顯示.
當圖片過大,超多GPU 的最大紋理尺寸時,圖片需要先由CPU進行預處理,這對CPU和GPU來實說都會帶來額外的資源消耗.目前來說,盡量不要讓圖片和視圖的大小超過iPhone 中的4096*4096.這個尺寸是iPhone紋理尺寸的上限.
視圖的混合
當多個視圖(或者說CALayer)重疊在一起顯示時,GPU會首先把他們混合到一起.如果視圖結構過于復雜,混合的過程也會消耗很多GPU 資源.為了減輕這種情況下的GPU消耗,應用應當盡量減少視圖數量和層次,并在不透明的視圖里標明opaque屬性以避免無用的Alpha通道合成.當然,這也可以用上面的方法,把多個視圖預先渲染為一張圖片來顯示.** 圖形生成**
CALayer
的border
,圓角,陰影,遮罩(mask),CASharpLayer
的矢量圖形顯示,通常會觸發離屏渲染(offscreen rendering)
,而離屏渲染通常發生在GPU中.當一個列表視圖中出現大量圓角的CALayer
,并且快速滑動時,可以觀察到GPU資源已經沾滿,而CPU資源消耗很少.這是界面仍然能正常滑動,但平均幀數轉嫁到CPU上去.對于只需要圓角的某些場合,也可以用一張已經繪制好的圓角覆蓋到原本視圖上面來模擬形同的視覺效果.把需要顯示的圖形在后臺線程繪制為圖片,避免使用圓角,陰影,遮罩等是比較徹底的結局方法.