卡頓產生的原因
在 VSync
訊號到來後,系統圖形服務會通過 CADisplayLink
等機制通知 App
,App
主執行緒開始在 CPU
中計算顯示內容,比如檢視的建立、佈局計算、圖片解碼、文字繪製等。隨後 CPU
會將計算好的內容提交到 GPU
去,由 GPU
進行變換、合成、渲染。隨後 GPU
會把渲染結果提交到幀緩衝區去,等待下一次 VSync
訊號到來時顯示到螢幕上。由於垂直同步的機制,如果在一個 VSync
時間內,CPU
或者 GPU
沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時螢幕會保留之前的內容不變。這就是介面卡頓的原因。
在開發中,CPU
和GPU
中任何一個壓力過大,都會導致掉幀現象,所以在開發時,也需要分別對CPU
和GPU
壓力進行評估和優化。
iOS 裝置中的 CPU & GPU
CPU
載入資源,物件建立,物件調整,物件銷燬,佈局計算,Autolayout,文字計算,文字渲染,圖片的解碼, 影象的繪製(Core Graphics)都是在CPU
上面進行的。
GPU
GPU
是一個專門為圖形高併發計算而量身定做的處理單元,比CPU
使用更少的電來完成工作並且GPU
的浮點計算能力要超出CPU
很多。
GPU
的渲染效能要比CPU
高效很多,同時對系統的負載和消耗也更低一些,所以在開發中,我們應該儘量讓CPU
負責主執行緒的UI
調動,把圖形顯示相關的工作交給GPU
來處理,當涉及到光柵化等一些工作時,CPU
也會參與進來,這點在後面再詳細描述。
相對於CPU
來說,GPU
能幹的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合(合成)並渲染,然後輸出到螢幕上。通常你所能看到的內容,主要也就是紋理(圖片)和形狀(三角模擬的向量圖形)兩類。
CPU 和 GPU 的協作
由上圖可知,要在螢幕上顯示檢視,需要CPU
和GPU
一起協作,CPU
計算好顯示的內容提交到GPU
,GPU
渲染完成後將結果放到幀快取區,隨後視訊控制器會按照 VSync
訊號逐行讀取幀緩衝區的資料,經過可能的數模轉換傳遞給顯示器顯示。
緩衝機制
iOS
使用的是雙緩衝機制。即GPU
會預先渲染好一幀放入一個緩衝區內(前幀快取),讓視訊控制器讀取,當下一幀渲染好後,GPU
會直接把視訊控制器的指標指向第二個緩衝器(後幀快取)。當你視訊控制器已經讀完一幀,準備讀下一幀的時候,GPU
會等待顯示器的VSync
訊號發出後,前幀快取和後幀快取會瞬間切換,後幀快取會變成新的前幀快取,同時舊的前幀快取會變成新的後幀快取。
優化方案
在YY
大神的 iOS 保持介面流暢的技巧中詳細介紹了 CPU 資源消耗原因和解決方案和 GPU 資源消耗原因和解決方案,這裡麵包括了開發中的大部分場景,可以幫助我們快速定位卡頓的原因,迅速解決卡頓。
下面是一些常見的優化方案!
TableViewCell 複用
在cellForRowAtIndexPath:
回撥的時候只建立例項,快速返回cell
,不繫結資料。在willDisplayCell: forRowAtIndexPath:
的時候繫結資料(賦值)。
高度快取
在tableView
滑動時,會不斷呼叫heightForRowAtIndexPath:
,當 cell
高度需要自適應時,每次回撥都要計算高度,會導致 UI 卡頓。為了避免重複無意義的計算,需要快取高度。
怎麼快取?
- 字典,NSCache。
- UITableView-FDTemplateLayoutCell
檢視層級優化
不要動態建立檢視
- 在記憶體可控的前提下,快取
subview
。 - 善用
hidden
。
減少檢視層級
- 減少
subviews
個數,用layer
繪製元素。 - 少用
clearColor
,maskToBounds
,陰影效果等。
減少多餘的繪製操作
圖片
- 不要用
JPEG
的圖片,應當使用PNG
圖片。 - 子執行緒預解碼(
Decode
),主執行緒直接渲染。因為當image
沒有Decode
,直接賦值給imageView
會進行一個Decode
操作。 - 優化圖片大小,儘量不要動態縮放(
contentMode
)。 - 儘可能將多張圖片合成為一張進行顯示。
減少透明 view
使用透明view
會引起blending
,在iOS
的圖形處理中,blending
主要指的是混合畫素顏色的計算。最直觀的例子就是,我們把兩個圖層疊加在一起,如果第一個圖層的透明的,則最終畫素的顏色計算需要將第二個圖層也考慮進來。這一過程即為Blending
。
會導致blending
的原因:
UIView
的alpha
<1
。UIImageView
的image
含有alpha channel
(即使UIImageView
的alpha
是1
,但只要image
含有透明通道,則仍會導致blending
)。
為什麼blending
會導致效能的損失?
原因是很直觀的,如果一個圖層是不透明的,則系統直接顯示該圖層的顏色即可。而如果圖層是透明的,則會引起更多的計算,因為需要把另一個的圖層也包括進來,進行混合後的顏色計算。
opaque
設定為YES
,減少效能消耗,因為GPU
將不會做任何合成,而是簡單從這個層拷貝。
減少離屏渲染
離屏渲染指的是在影象在繪製到當前螢幕前,需要先進行一次渲染,之後才繪製到當前螢幕。
OpenGL
中,GPU
螢幕渲染有以下兩種方式:
-
On-Screen Rendering
即當前螢幕渲染,指的是GPU
的渲染操作是在當前用於顯示的螢幕緩衝區中進行。 -
Off-Screen Rendering
即離屏渲染,指的是GPU
在當前螢幕緩衝區以外新開闢一個緩衝區進行渲染操作。
為什麼離屏渲染會發生卡頓?主要包括兩方面內容:
- 建立新的緩衝區。
- 上下文切換,離屏渲染的整個過程,需要多次切換上下文環境(
CPU
渲染和GPU
切換),先是從當前螢幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以後,將離屏緩衝區的渲染結果顯示到螢幕上又需要將上下文環境從離屏切換到當前螢幕。而上下文環境的切換是要付出很大代價的。
設定了以下屬性時,都會觸發離屏渲染:
-
layer.shouldRasterize
,光柵化 -
layer.mask
,遮罩 -
layer.allowsGroupOpacity
為YES
,layer.opacity
的值小於1.0
-
layer.cornerRadius
,並且設定layer.masksToBounds
為YES
。可以使用剪下過的圖片,或者使用layer
畫來解決。 -
layer.shadows
,(表示相關的shadow開頭的屬性),使用shadowPath
代替。兩種不同方式來繪製陰影: 不使用
shadowPath
使用
shadowPath
效能差別,如下圖:
離屏渲染的優化建議
- 使用
ShadowPath
指定layer
陰影效果路徑。 - 使用非同步進行
layer
渲染(Facebook
開源的非同步繪製框架AsyncDisplayKit
)。 - 設定
layer
的opaque
值為YES
,減少複雜圖層合成。 - 儘量使用不包含透明(
alpha
)通道的圖片資源。 - 儘量設定
layer
的大小值為整形值。 - 直接讓美工把圖片切成圓角進行顯示,這是效率最高的一種方案。
- 很多情況下使用者上傳圖片進行顯示,可以在客戶端處理圓角。
- 使用程式碼手動生成圓角
image
設定到要顯示的View
上,利用UIBezierPath
(Core Graphics
框架)畫出來圓角圖片。
合理使用光柵化 shouldRasterize
光柵化是把GPU
的操作轉到CPU
上,生成點陣圖快取,直接讀取複用。
優點:
CALayer
會被光柵化為bitmap
,shadows
、cornerRadius
等效果會被快取。
缺點:
- 更新已經光柵化的
layer
,會造成離屏渲染。 bitmap
超過100ms
沒有使用就會移除。- 受系統限制,快取的大小為 2.5X Screen Size。
shouldRasterize
適合靜態頁面顯示,動態頁面會增加開銷。如果設定了shouldRasterize
為 YES
,那也要記住設定rasterizationScale
為contentsScale
。
非同步渲染
在子執行緒繪製,主執行緒渲染。例如 VVeboTableViewDemo
理性使用-drawRect:
大家或許感到奇怪,有不少開發者在發有關效能優化的部落格當中指出使用-drawRect:
來優化效能。但是我這裡不太建議大家未經思考的使用-drawRect:
方法。原因如下:
當你使用UIImageView
在載入一個檢視的時候,這個檢視雖然依然有CALayer
,但是卻沒有申請到一個後備的儲存,取而代之的是使用一個使用螢幕外渲染,將CGImageRef
作為內容,並用渲染服務將圖片資料繪製到幀的緩衝區,就是顯示到螢幕上,當我們滾動檢視的時候,這個檢視將會重新載入,浪費效能。所以對於使用-drawRect:
方法,更傾向於使用CALayer
來繪製圖層。因為使用CALayer
的-drawInContext:
,Core Animation
將會為這個圖層申請一個後備儲存,用來儲存那些方法繪製進來的點陣圖。那些方法內的程式碼將會執行在 CPU
上,結果將會被上傳到GPU
。這樣做的效能更為好些。
靜態介面建議使用-drawRect:
的方式,動態頁面不建議。
按需載入
- 區域性重新整理,重新整理一個
cell
就能解決的,堅決不重新整理整個section
或者整個tableView
,重新整理最小單元元素。 - 利用
runloop
提高滑動流暢性,在滑動停止的時候再載入內容,像那種一閃而過的(快速滑動),就沒有必要載入,可以使用預設的佔位符填充內容。
關於效能測試
在出現影象效能問題,滑動,動畫不夠流暢之後,我們首先要做的就是定位出問題的所在。而這個過程並不是只靠經驗和窮舉法探索,我們應該用有脈絡,有順序的科學的手段進行探索。
首先,我們要有一個定位問題的模式。我們可以按照這樣的順序來逐步定位,發現問題。
- 定位幀率,為了給使用者流暢的感受,我們需要保持幀率在
60
幀左右。當遇到問題後,我們首先檢查一下幀率是否保持在60
幀。 - 定位瓶頸,究竟是
CPU
還是GPU
。我們希望佔用率越少越好,一是為了流暢性,二也節省了電力。 - 檢查有沒有做無必要的
CPU
渲染,例如有些地方我們重寫了drawRect:
,而其實是我們不需要也不應該的。我們希望GPU
負責更多的工作。 - 檢查有沒有過多的離屏渲染,這會耗費
GPU
的資源,像前面已經分析的到的。離屏渲染會導致GPU
需要不斷地onScreen
和offscreen
進行上下文切換。我們希望有更少的離屏渲染。 - 檢查我們有無過多的
Blending
,GPU
渲染一個不透明的圖層更省資源。 - 檢查圖片的格式是否為常用格式,大小是否正常。如果一個圖片格式不被
GPU
所支援,則只能通過CPU
來渲染。一般我們在iOS
開發中都應該用PNG
格式,之前閱讀過的一些資料也有指出蘋果特意為PNG
格式做了渲染和壓縮演算法上的優化。 - 檢查是否有耗費資源多的
View
或效果,我們需要合理有節制的使用。 - 最後,我們需要檢查在我們
View
層級中是否有不正確的地方。例如有時我們不斷的新增或移除View
,有時就會在不經意間導致bug
的發生。
測試工具:
Core Animation
,Instruments
裡的圖形效能問題的測試工具。view debugging
,Xcode 自帶的,檢視層級。reveal
,檢視層級。
參考文章
- 繪製畫素到螢幕上
- iOS圖形原理與離屏渲染,在1.4.1中,
這也是為什麼 CALayer 有一個叫做 opaque 的屬性了。如果這個屬性為 NO,GPU 將不會做任何合成,而是簡單從這個層拷貝,不需要考慮它下方的任何東西(因為都被它遮擋住了)。
中的opaque
屬性為NO
,GPU
將不會做任何合成,這句話時錯誤的,應該是為YES
,GPU
才不會做任何合成。 - iOS 保持介面流暢的技巧
- Advanced Graphics and Animations for iOS Apps(session 419)
- 使用 ASDK 效能調優 - 提升 iOS 介面的渲染效能
- Designing for iOS: Graphics & Performance
- iOS離屏渲染之優化分析
- iOS檢視渲染以及效能優化總結
- iOS 離屏渲染
- 深刻理解移動端優化之離屏渲染
- iOS 流暢度效能優化、CPU、GPU、離屏渲染
- iOS 圖形效能優化錦集
- 離屏渲染優化詳解:例項示範+效能測試
如有內容錯誤,歡迎 issue 指正。
轉載請註明出處!