iOS界面卡頓及解決方案

卡頓一般是由於CPU或者GPU沒有完成內容提交,以至於那一幀會被丟掉,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。

CPU相關

對象創建

對象的創建會分配內存、調整屬性、甚至還有讀取文件等操作,比較消耗CPU資源。儘量用輕量的對象代替重量的對象。比如CALayer比UIView要輕量許多,如果不需要響應事件,顯然用CALayer更加合適。如果不涉及UI操作則儘量放到後臺線程去創建。通過Storyboard創建視圖對象時,其資源消耗會比直接通過代碼創建對象要大很多。
儘量推遲對象創建時間,並把對象的創建分散到多個任務。如果對象可以複用,儘量這類對象放到緩衝池中。

對象調整

對象的調整也很消耗CPU。比如對UIView的顯示相關屬性(frame、bounds、transform等)調整、視圖層次調整。

對象銷燬

對象銷燬雖然消耗資源不多,但累計起來也是不容忽視的。

佈局計算

視圖佈局的計算是最爲常見的消耗CPU資源的地方。如果能在後臺線程提前計算好視圖佈局、並且對視圖佈局進行緩存,那麼這塊兒基本就不會產生性能問題。

Autolayout

Autolayout是蘋果提倡的技術,在大部分情況下也能很好的提升開發效率,但是對於複雜的視圖來說常常會產生嚴重的性能問題。隨着視圖數量的增長,Autolayout帶來的CPU消耗會呈指數級增長。如果不想手動調整frame屬性,可以用一些工具方法替代(left、right、top、button、width、height等)。

文本計算

如果一個界面中包含大量文本,文本的寬高計算會佔用很大一部分資源,並且不可避免。可以參考一下UILabel內部的實現方式:

//計算文本高度
[NSAttributeString boundingRectWithSize: options: context: ];
//繪製文本
[NSAttributeString drawWithRect: options: context: ];

儘管這兩個方法性能不錯,但仍需放在後臺線程以避免阻塞主線程。
如果用CoreText繪製文本,就可以先生成CoreText排版對象,然後自己計算。並且CoreText還能夠保留供稍後繪製使用。

文本渲染

屏幕上能看到的所有文本內容控件,在底層都是通過CoreText排版、繪製位Bitmap顯示的。常見的文本控件其排版和繪製都是在主線程進行的,當顯示大量文本時,CPU的壓力會非常大。對此解決方案只有一個,那就是自定義文本控件,用TextKit或最底層的CoreText對文本異步繪製。CoreText對象創建後可直接獲取文本的寬高等信息,避免了多次計算(調整UILabel大小時計算一遍、UILabel繪製時內部再計算一遍)。CoreText對象佔用內存較少,可以緩存下來以備稍後多次渲染。

圖片的解碼

當使用UIImage或CGImageSource的那幾個方法創建圖片時,圖片數據並不會立刻解碼。圖片設置到UIImageView或者CALayer.contents中去,並且CALayer被提交到GPU前,CGImage中的數據纔會得到解碼。這一步發生在主線程並且不可避免。如果想要繞開這個機制,常見的做法是在後臺線程先把圖片繪製到CGBitmapContext中,然後從Bitmap中直接創建圖片。目前常見的網絡圖片庫都自帶這個功能。

圖像的繪製

圖像的繪製一般是指用那些以CG開頭的方法把圖像繪製到畫布中,然後從畫布創建圖片並顯示的一個過程。由於CoreGraphic方法通常都是線程安全的,所以圖像繪製很容易放倒後臺線程執行。

GPU相關

相對於CPU來說,GPU能幹的事情比較單一:接收提交的紋理和頂點描述,應用變換、混合並渲染,然後輸出到屏幕。通常所能看到的內容主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。

紋理的渲染

所有的Bitmap,包括圖片、文本、柵格化的內容,最終都要有內存提交到緩存,綁定爲GPU紋理。當在較短時間顯示大量圖片時(比如tableview存在很多圖片並且快速滑動時),CPU佔用率很低,GPU佔用非常高,界面仍會掉幀。避免這種情況的方法只能是隻能儘量減少在短時間內顯示圖片數量,儘可能將多張圖片合成一張進行顯示。

視圖的混合

當多個視圖(或者說CALayer)重疊在一起顯示時,GPU會首先把它們混合在一起。如果視圖結果過於複雜,混合的過程也會消耗很多GPU資源。爲了減輕這種情況的GPU消耗,應用應當儘量減少視圖數量和層次,並在不透明的的視圖裏標明opaque屬性以避免無用的alpha通道合成。

圖形的生成

CALayer的border、圓角、陰影、遮罩(mask),CAShapLayer的矢量圖形顯示,通常會觸發離屏渲染(offscreen rendering),而離屏渲染通常發生在GPU中。當一個列表視圖中出現大量圓角的CALayer並且快速滑動時,可以觀察到GPU資源已佔滿而CPU資源消耗很少,這時界面幀數會降到很低。爲了避免這種情況可以嘗試開啓CALayer.shouldRasterize屬性,但這會把原本離屏渲染的操作轉嫁到CPU上去。對於只需要圓角的某些場合,也可以用一張已經繪製好的圓角圖片覆蓋到原本視圖上;最徹底的解決辦法就是把需要顯示的圓形在後臺線程繪製爲圖片,避免使用圓角、陰影、遮罩等屬性。

AsyncDisplayKit

Facebook發佈了其iOS UI框架AsyncDisplayKit(ASDK)的正式版,這個框架被用於Facebook自家的應用Paper,能夠提高UI的流暢性並縮短響應時間。主要是把原來在主線程的處理(解碼圖像、佈局、渲染等)放到後臺並且可以利用不同的CPU核心。

ASDK 的基本原理

asdk_design
ASDK認爲阻塞主線程的任務主要分爲上面三大類。文本和佈局的計算、渲染、解碼、繪製都可以通過各種方式異步執行,但UIKit和Core Animation相關操作必須在主線程執行。ASDK的目標就是儘量把這些任務從主線程挪走,挪不動的就儘量優化性能。
爲了達成這一目標,ASDK 嘗試對 UIKit 組件進行封裝:
layer_backed_view
這是常見的UIView和CALayer的關係:View持有Layer用於顯示,View中大部分顯示屬性實際是從Layer映射而來;Layer的代理在這裏是View,當其屬性改變動畫產生時,View能夠得到通知。UIView和CALayer不是線程安全的,並且只能在主線程創建、訪問和銷燬。
view_backed_node
ASDK 爲此創建了 ASDisplayNode 類,包裝了常見的視圖屬性(比如 frame、bounds、alpha、transform、backgroundColor、superNode、subNodes 等),然後它用 UIView—>CALayer 相同的方式,實現了 ASNode—>UIView 這樣一個關係。
layer_backed_view
當不需要響應觸摸事件時,ASDisplayNode可以被設置爲layer backed,即ASDisplayNode充當了原來UIView的功能,節省了更多資源。
與 UIView 和 CALayer 不同,ASDisplayNode 是線程安全的,它可以在後臺線程創建和修改。Node 剛創建時,並不會在內部新建 UIView 和 CALayer,直到第一次在主線程訪問 view 或 layer 屬性時,它纔會在內部生成對應的對象。當它的屬性(比如frame、transform)改變後,它並不會立刻同步到其持有的 view 或 layer 去,而是把被改變的屬性保存到內部的一箇中間變量,稍後在需要時,再通過某個機制一次性設置到內部的 view 或 layer。
通過模擬和封裝 UIView、CALayer,開發者可以把代碼中的 UIView 替換爲 ASNode,很大的降低了開發和學習成本,同時能獲得 ASDK 底層大量的性能優化。爲了方便使用, ASDK 把大量常用控件都封裝成了 ASNode 的子類,比如 Button、Control、Cell、Image、ImageView、Text、TableView、CollectionView 等。利用這些控件,開發者可以儘量避免直接使用 UIKit 相關控件,以獲得更完整的性能提升。

ASDK 的圖層預合成

有時一個 layer 會包含很多 sub-layer,而這些 sub-layer 並不需要響應觸摸事件,也不需要進行動畫和位置調整。ASDK 爲此實現了一個被稱爲 pre-composing 的技術,可以把這些 sub-layer 合成渲染爲一張圖片。開發時,ASNode 已經替代了 UIView 和 CALayer;直接使用各種 Node 控件並設置爲 layer backed 後,ASNode 甚至可以通過預合成來避免創建內部的 UIView 和 CALayer。
通過這種方式,把一個大的層級,通過一個大的繪製方法繪製到一張圖上,性能會獲得很大提升。CPU避免了創建UIKit對象的資源消耗,GPU避免了多張紋理合成和渲染的消耗,更少的 Bitmap 也意味着更少的內存佔用。

ASDK異步併發操作

自 iPhone 4S 起,iDevice 已經都是雙核 CPU 了。充分利用多核的優勢、併發執行任務對保持界面流暢有很大作用。ASDK 把佈局計算、文本排版、圖片/文本/圖形渲染等操作都封裝成較小的任務,並利用 GCD 異步併發執行。如果開發者使用了 ASNode 相關的控件,那麼這些併發操作會自動在後臺進行,無需進行過多配置。

RunLoop任務分發

Runloop work distribution 是 ASDK 比較核心的一個技術。
ios_vsync_runloop
iOS 的顯示系統是由 VSync 信號驅動的,VSync 信號由硬件時鐘生成,每秒鐘發出 60 次(這個值取決設備硬件,比如 iPhone 真機上通常是 59.97)。iOS 圖形服務接收到 VSync 信號後,會通過 IPC 通知到 App 內。App 的 Runloop 在啓動後會註冊對應的 CFRunLoopSource 通過 mach_port 接收傳過來的時鐘信號通知,隨後 Source 的回調會驅動整個 App 的動畫與顯示。

Core Animation 在 RunLoop 中註冊了一個 Observer,監聽了 BeforeWaiting 和 Exit 事件。這個 Observer 的優先級是 2000000,低於常見的其他 Observer。當一個觸摸事件到來時,RunLoop 被喚醒,App 中的代碼會執行一些操作,比如創建和調整視圖層級、設置 UIView 的 frame、修改 CALayer 的透明度、爲視圖添加一個動畫;這些操作最終都會被 CALayer 捕獲,並通過 CATransaction 提交到一箇中間狀態去(CATransaction 的文檔略有提到這些內容,但並不完整)。當上面所有操作結束後,RunLoop 即將進入休眠(或者退出)時,關注該事件的 Observer 都會得到通知。這時 CA 註冊的那個 Observer 就會在回調中,把所有的中間狀態合併提交到 GPU 去顯示;如果此處有動畫,CA 會通過 DisplayLink 等機制多次觸發相關流程。

ASDK 在此處模擬了 Core Animation 的這個機制:所有針對 ASNode 的修改和提交,總有些任務是必需放入主線程執行的。當出現這種任務時,ASNode 會把任務用 ASAsyncTransaction(Group) 封裝並提交到一個全局的容器去。ASDK 也在 RunLoop 中註冊了一個 Observer,監視的事件和 CA 一樣,但優先級比 CA 要低。當 RunLoop 進入休眠前、CA 處理完事件後,ASDK 就會執行該 loop 內提交的所有任務。具體代碼見這個文件:ASAsyncTransactionGroup

通過這種機制,ASDK 可以在合適的機會把異步、併發的操作同步到主線程去,並且能獲得不錯的性能。

其他

ASDK 中還有封裝很多高級的功能,比如滑動列表的預加載、V2.0添加的新的佈局模式等。ASDK 是一個很龐大的庫,它本身並不推薦你把整個 App 全部都改爲 ASDK 驅動,把最需要提升交互性能的地方用 ASDK 進行優化就足夠了。

發佈了29 篇原創文章 · 獲贊 6 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章