這篇文章會很是詳細的分析 iOS 界面構建中的各類性能問題以及對應的解決思路,同時給出一個開源的微博列表實現,經過實際的代碼展現如何構建流暢的交互。node
Index
演示項目
屏幕顯示圖像的原理
卡頓產生的緣由和解決方案
CPU 資源消耗緣由和解決方案
GPU 資源消耗緣由和解決方案
AsyncDisplayKit
ASDK 的由來
ASDK 的資料
ASDK 的基本原理
ASDK 的圖層預合成
ASDK 異步併發操做
Runloop 任務分發
微博 Demo 性能優化技巧
預排版
預渲染
異步繪製
全局併發控制
更高效的異步圖片加載
其餘能夠改進的地方
如何評測界面的流暢度ios
在開始技術討論前,你能夠先下載我寫的 Demo 跑到真機上體驗一下:https://github.com/ibireme/YYKit。 Demo 裏包含一個微博的 Feed 列表、發佈視圖,還包含一個 Twitter 的 Feed 列表。爲了公平起見,全部界面和交互我都從官方應用原封不動的抄了過來,數據也都是從官方應用抓取的。你也能夠本身抓取數據替換掉 Demo 中的數據,方便進行對比。儘管官方應用背後的功能更多更爲複雜,但不至於會帶來太大的交互性能差別。git
這個 Demo 最低能夠運行在 iOS 6 上,因此你能夠把它跑到老設備上體驗一下。在個人測試中,即便在 iPhone 4S 或者 iPad 3 上,Demo 列表在快速滑動時仍然能保持 50~60 FPS 的流暢交互,而其餘諸如微博、朋友圈等應用的列表視圖在滑動時已經有很嚴重的卡頓了。github
微博的 Demo 有大約四千行代碼,Twitter 的只有兩千行左右代碼,第三方庫只用到了 YYKit,文件數量比較少,方便查看。好了,下面是正文。緩存
首先從過去的 CRT 顯示器原理提及。CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成後顯示器就呈現一幀畫面,隨後電子槍回到初始位置繼續下一次掃描。爲了把顯示器的顯示過程和系統的視頻控制器進行同步,顯示器(或者其餘硬件)會用硬件時鐘產生一系列的定時信號。當電子槍換到新的一行,準備進行掃描時,顯示器會發出一個水平同步信號(horizonal synchronization),簡稱 HSync;而當一幀畫面繪製完成後,電子槍回覆到原位,準備畫下一幀前,顯示器會發出一個垂直同步信號(vertical synchronization),簡稱 VSync。顯示器一般以固定頻率進行刷新,這個刷新率就是 VSync 信號產生的頻率。儘管如今的設備大都是液晶顯示屏了,但原理仍然沒有變。安全
一般來講,計算機系統中 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 資源。儘可能用輕量的對象代替重量的對象,能夠對性能有所優化。好比 CALayer 比 UIView 要輕量許多,那麼不須要響應觸摸事件的控件,用 CALayer 顯示會更加合適。若是對象不涉及 UI 操做,則儘可能放到後臺線程去建立,但惋惜的是包含有 CALayer 的控件,都只能在主線程建立和操做。經過 Storyboard 建立視圖對象時,其資源消耗會比直接經過代碼建立對象要大很是多,在性能敏感的界面裏,Storyboard 並非一個好的技術選擇。
儘可能推遲對象建立的時間,並把對象的建立分散到多個任務中去。儘管這實現起來比較麻煩,而且帶來的優點並很少,但若是有能力作,仍是要儘可能嘗試一下。若是對象能夠複用,而且複用的代價比釋放、建立新對象要小,那麼這類對象應當儘可能放到一個緩存池裏複用。
對象調整
對象的調整也常常是消耗 CPU 資源的地方。這裏特別說一下 CALayer:CALayer 內部並無屬性,當調用屬性方法時,它內部是經過運行時 resolveInstanceMethod 爲對象臨時添加一個方法,並把對應屬性值保存到內部的一個 Dictionary 裏,同時還會通知 delegate、建立動畫等等,很是消耗資源。UIView 的關於顯示相關的屬性(好比 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的,因此對 UIView 的這些屬性進行調整時,消耗的資源要遠大於通常的屬性。對此你在應用中,應該儘可能減小沒必要要的屬性修改。
當視圖層次調整時,UIView、CALayer 之間會出現不少方法調用與通知,因此在優化性能時,應該儘可能避免調整視圖層次、添加和移除視圖。
對象銷燬
對象的銷燬雖然消耗資源很少,但累積起來也是不容忽視的。一般當容器類持有大量對象時,其銷燬時的資源消耗就很是明顯。一樣的,若是對象能夠放到後臺線程去釋放,那就挪到後臺線程去。這裏有個小 Tip:把對象捕獲到 block 中,而後扔到後臺隊列去隨便發送個消息以免編譯器警告,就可讓對象在後臺線程銷燬了。
1
2
3
4
5
|
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
|
佈局計算
視圖佈局的計算是 App 中最爲常見的消耗 CPU 資源的地方。若是能在後臺線程提早計算好視圖佈局、而且對視圖佈局進行緩存,那麼這個地方基本就不會產生性能問題了。
不論經過何種技術對視圖進行佈局,其最終都會落到對 UIView.frame/bounds/center 等屬性的調整上。上面也說過,對這些屬性的調整很是消耗資源,因此儘可能提早計算好佈局,在須要時一次性調整好對應屬性,而不要屢次、頻繁的計算和調整這些屬性。
Autolayout
Autolayout 是蘋果自己提倡的技術,在大部分狀況下也能很好的提高開發效率,可是 Autolayout 對於複雜視圖來講經常會產生嚴重的性能問題。隨着視圖數量的增加,Autolayout 帶來的 CPU 消耗會呈指數級上升。具體數據能夠看這個文章:http://pilky.me/36/。 若是你不想手動調整 frame 等屬性,你能夠用一些工具方法替代(好比常見的 left/right/top/bottom/width/height 快捷屬性),或者使用 ComponentKit、AsyncDisplayKit 等框架。
文本計算
若是一個界面中包含大量文本(好比微博微信朋友圈等),文本的寬高計算會佔用很大一部分資源,而且不可避免。若是你對文本顯示沒有特殊要求,能夠參考下 UILabel 內部的實現方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪製文本。儘管這兩個方法性能不錯,但仍舊須要放到後臺線程進行以免阻塞主線程。
若是你用 CoreText 繪製文本,那就能夠先生成 CoreText 排版對象,而後本身計算了,而且 CoreText 對象還能保留以供稍後繪製使用。
文本渲染
屏幕上能看到的全部文本內容控件,包括 UIWebView,在底層都是經過 CoreText 排版、繪製爲 Bitmap 顯示的。常見的文本控件 (UILabel、UITextView 等),其排版和繪製都是在主線程進行的,當顯示大量文本時,CPU 的壓力會很是大。對此解決方案只有一個,那就是自定義文本控件,用 TextKit 或最底層的 CoreText 對文本異步繪製。儘管這實現起來很是麻煩,但其帶來的優點也很是大,CoreText 對象建立好後,能直接獲取文本的寬高等信息,避免了屢次計算(調整 UILabel 大小時算一遍、UILabel 繪製時內部再算一遍);CoreText 對象佔用內存較少,能夠緩存下來以備稍後屢次渲染。
圖片的解碼
當你用 UIImage 或 CGImageSource 的那幾個方法建立圖片時,圖片數據並不會馬上解碼。圖片設置到 UIImageView 或者 CALayer.contents 中去,而且 CALayer 被提交到 GPU 前,CGImage 中的數據纔會獲得解碼。這一步是發生在主線程的,而且不可避免。若是想要繞開這個機制,常見的作法是在後臺線程先把圖片繪製到 CGBitmapContext 中,而後從 Bitmap 直接建立圖片。目前常見的網絡圖片庫都自帶這個功能。
圖像的繪製
圖像的繪製一般是指用那些以 CG 開頭的方法把圖像繪製到畫布中,而後從畫布建立圖片並顯示這樣一個過程。這個最多見的地方就是 [UIView drawRect:] 裏面了。因爲 CoreGraphic 方法一般都是線程安全的,因此圖像的繪製能夠很容易的放到後臺線程進行。一個簡單異步繪製的過程大體以下(實際狀況會比這個複雜得多,但原理基本一致):
1
2
3
4
5
6
7
8
9
10
11
|
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
|
相對於 CPU 來講,GPU 能幹的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合並渲染,而後輸出到屏幕上。一般你所能看到的內容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。
紋理的渲染
全部的 Bitmap,包括圖片、文本、柵格化的內容,最終都要由內存提交到顯存,綁定爲 GPU Texture。不管是提交到顯存的過程,仍是 GPU 調整和渲染 Texture 的過程,都要消耗很多 GPU 資源。當在較短期顯示大量圖片時(好比 TableView 存在很是多的圖片而且快速滑動時),CPU 佔用率很低,GPU 佔用很是高,界面仍然會掉幀。避免這種狀況的方法只能是儘可能減小在短期內大量圖片的顯示,儘量將多張圖片合成爲一張進行顯示。
當圖片過大,超過 GPU 的最大紋理尺寸時,圖片須要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。目前來講,iPhone 4S 以上機型,紋理尺寸上限都是 4096×4096,更詳細的資料能夠看這裏:iosres.com。因此,儘可能不要讓圖片和視圖的大小超過這個值。
視圖的混合 (Composing)
當多個視圖(或者說 CALayer)重疊在一塊兒顯示時,GPU 會首先把他們混合到一塊兒。若是視圖結構過於複雜,混合的過程也會消耗不少 GPU 資源。爲了減輕這種狀況的 GPU 消耗,應用應當儘可能減小視圖數量和層次,並在不透明的視圖裏標明 opaque 屬性以免無用的 Alpha 通道合成。固然,這也能夠用上面的方法,把多個視圖預先渲染爲一張圖片來顯示。
圖形的生成。
CALayer 的 border、圓角、陰影、遮罩(mask),CASharpLayer 的矢量圖形顯示,一般會觸發離屏渲染(offscreen rendering),而離屏渲染一般發生在 GPU 中。當一個列表視圖中出現大量圓角的 CALayer,而且快速滑動時,能夠觀察到 GPU 資源已經佔滿,而 CPU 資源消耗不多。這時界面仍然能正常滑動,但平均幀數會降到很低。爲了不這種狀況,能夠嘗試開啓 CALayer.shouldRasterize 屬性,但這會把本來離屏渲染的操做轉嫁到 CPU 上去。對於只須要圓角的某些場合,也能夠用一張已經繪製好的圓角圖片覆蓋到本來視圖上面來模擬相同的視覺效果。最完全的解決辦法,就是把須要顯示的圖形在後臺線程繪製爲圖片,避免使用圓角、陰影、遮罩等屬性。
AsyncDisplayKit 是 Facebook 開源的一個用於保持 iOS 界面流暢的庫,我從中學到了不少東西,因此下面我會花較大的篇幅來對其進行介紹和分析。
ASDK 的做者是 Scott Goodson (Linkedin),
他曾經在蘋果工做,負責 iOS 的一些內置應用的開發,好比股票、計算器、地圖、鐘錶、設置、Safari 等,固然他也參與了 UIKit framework 的開發。後來他加入 Facebook 後,負責 Paper 的開發,建立並開源了 AsyncDisplayKit。目前他在 Pinterest 和 Instagram 負責 iOS 開發和用戶體驗的提高等工做。
ASDK 自 2014 年 6 月開源,10 月發佈 1.0 版。目前 ASDK 即將要發佈 2.0 版。
V2.0 增長了更多佈局相關的代碼,ComponentKit 團隊爲此貢獻不少。
如今 Github 的 master 分支上的版本是 V1.9.1,已經包含了 V2.0 的所有內容。
想要了解 ASDK 的原理和細節,最好從下面幾個視頻開始:
2014.10.15 NSLondon – Scott Goodson – Behind AsyncDisplayKit
2015.03.02 MCE 2015 – Scott Goodson – Effortless Responsiveness with AsyncDisplayKit
2015.10.25 AsyncDisplayKit 2.0: Intelligent User Interfaces – NSSpain 2015
前兩個視頻內容大同小異,都是介紹 ASDK 的基本原理,附帶介紹 POP 等其餘項目。
後一個視頻增長了 ASDK 2.0 的新特性的介紹。
除此以外,還能夠到 Github Issues 裏看一下 ASDK 相關的討論,下面是幾個比較重要的內容:
關於 Runloop Dispatch
關於 ComponentKit 和 ASDK 的區別
爲何不支持 Storyboard 和 Autolayout
如何評測界面的流暢度
以後,還能夠到 Google Groups 來查看和討論更多內容:
https://groups.google.com/forum/#!forum/asyncdisplaykit
ASDK 認爲,阻塞主線程的任務,主要分爲上面這三大類。文本和佈局的計算、渲染、解碼、繪製均可以經過各類方式異步執行,但 UIKit 和 Core Animation 相關操做必需在主線程進行。ASDK 的目標,就是儘可能把這些任務從主線程挪走,而挪不走的,就儘可能優化性能。
爲了達成這一目標,ASDK 嘗試對 UIKit 組件進行封裝:
這是常見的 UIView 和 CALayer 的關係:View 持有 Layer 用於顯示,View 中大部分顯示屬性實際是從 Layer 映射而來;Layer 的 delegate 在這裏是 View,當其屬性改變、動畫產生時,View 可以獲得通知。UIView 和 CALayer 不是線程安全的,而且只能在主線程建立、訪問和銷燬。
ASDK 爲此建立了 ASDisplayNode 類,包裝了常見的視圖屬性(好比 frame/bounds/alpha/transform/backgroundColor/superNode/subNodes 等),而後它用 UIView->CALayer 相同的方式,實現了 ASNode->UIView 這樣一個關係。
當不須要響應觸摸事件時,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 相關控件,以得到更完整的性能提高。
有時一個 layer 會包含不少 sub-layer,而這些 sub-layer 並不須要響應觸摸事件,也不須要進行動畫和位置調整。ASDK 爲此實現了一個被稱爲 pre-composing 的技術,能夠把這些 sub-layer 合成渲染爲一張圖片。開發時,ASNode 已經替代了 UIView 和 CALayer;直接使用各類 Node 控件並設置爲 layer backed 後,ASNode 甚至能夠經過預合成來避免建立內部的 UIView 和 CALayer。
經過這種方式,把一個大的層級,經過一個大的繪製方法繪製到一張圖上,性能會得到很大提高。CPU 避免了建立 UIKit 對象的資源消耗,GPU 避免了多張 texture 合成和渲染的消耗,更少的 bitmap 也意味着更少的內存佔用。
自 iPhone 4S 起,iDevice 已經都是雙核 CPU 了,如今的 iPad 甚至已經更新到 3 核了。充分利用多核的優點、併發執行任務對保持界面流暢有很大做用。ASDK 把佈局計算、文本排版、圖片/文本/圖形渲染等操做都封裝成較小的任務,並利用 GCD 異步併發執行。若是開發者使用了 ASNode 相關的控件,那麼這些併發操做會自動在後臺進行,無需進行過多配置。
Runloop work distribution 是 ASDK 比較核心的一個技術,ASDK 的介紹視頻和文檔中都沒有詳細展開介紹,因此這裏我會多作一些分析。若是你對 Runloop 還不太瞭解,能夠看一下我以前的文章深刻理解RunLoop,裏面對 ASDK 也有所說起。
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 進行優化就足夠了。
我爲了演示 YYKit 的功能,實現了微博和 Twitter 的 Demo,併爲它們作了很多性能優化,下面就是優化時用到的一些技巧。
當獲取到 API JSON 數據後,我會把每條 Cell 須要的數據都在後臺線程計算並封裝爲一個佈局對象 CellLayout。CellLayout 包含全部文本的 CoreText 排版結果、Cell 內部每一個控件的高度、Cell 的總體高度。每一個 CellLayout 的內存佔用並很少,因此當生成後,能夠所有緩存到內存,以供稍後使用。這樣,TableView 在請求各個高度函數時,不會消耗任何多餘計算量;當把 CellLayout 設置到 Cell 內部時,Cell 內部也不用再計算佈局了。
對於一般的 TableView 來講,提早在後臺計算好佈局結果是很是重要的一個性能優化點。爲了達到最高性能,你可能須要犧牲一些開發速度,不要用 Autolayout 等技術,少用 UILabel 等文本控件。但若是你對性能的要求並不那麼高,能夠嘗試用 TableView 的預估高度的功能,並把每一個 Cell 高度緩存下來。這裏有個來自百度知道團隊的開源項目能夠很方便的幫你實現這一點:FDTemplateLayoutCell。
微博的頭像在某次改版中換成了圓形,因此我也跟進了一下。當頭像下載下來後,我會在後臺線程將頭像預先渲染爲圓形並單獨保存到一個 ImageCache 中去。
對於 TableView 來講,Cell 內容的離屏渲染會帶來較大的 GPU 消耗。在 Twitter Demo 中,我爲了圖省事兒用到了很多 layer 的圓角屬性,你能夠在低性能的設備(好比 iPad 3)上快速滑動一下這個列表,能感覺到雖然列表並無較大的卡頓,可是總體的平均幀數降了下來。用 Instument 查看時可以看到 GPU 已經滿負荷運轉,而 CPU 卻比較悠閒。爲了不離屏渲染,你應當儘可能避免使用 layer 的 border、corner、shadow、mask 等技術,而儘可能在後臺線程預先繪製好對應內容。
我只在顯示文本的控件上用到了異步繪製的功能,但效果很不錯。我參考 ASDK 的原理,實現了一個簡單的異步繪製控件。這塊代碼我單獨提取出來,放到了這裏:YYAsyncLayer。YYAsyncLayer 是 CALayer 的子類,當它須要顯示內容(好比調用了 [layer setNeedDisplay])時,它會向 delegate,也就是 UIView 請求一個異步繪製的任務。在異步繪製時,Layer 會傳遞一個 BOOL(^isCancelled)() 這樣的 block,繪製代碼能夠隨時調用該 block 判斷繪製任務是否已經被取消。
當 TableView 快速滑動時,會有大量異步繪製任務提交到後臺線程去執行。可是有時滑動速度過快時,繪製任務尚未完成就可能已經被取消了。若是這時仍然繼續繪製,就會形成大量的 CPU 資源浪費,甚至阻塞線程並形成後續的繪製任務遲遲沒法完成。個人作法是儘可能快速、提早判斷當前繪製任務是否已經被取消;在繪製每一行文本前,我都會調用 isCancelled() 來進行判斷,保證被取消的任務能及時退出,不至於影響後續操做。
目前有些第三方微博客戶端(好比 VVebo、墨客等),使用了一種方式來避免高速滑動時 Cell 的繪製過程,相關實現見這個項目:VVeboTableViewDemo。它的原理是,當滑動時,鬆開手指後,馬上計算出滑動中止時 Cell 的位置,並預先繪製那個位置附近的幾個 Cell,而忽略當前滑動中的 Cell。這個方法比較有技巧性,而且對於滑動性能來講提高也很大,惟一的缺點就是快速滑動中會出現大量空白內容。若是你不想實現比較麻煩的異步繪製但又想保證滑動的流暢性,這個技巧是個不錯的選擇。
當我用 concurrent queue 來執行大量繪製任務時,偶爾會遇到這種問題:
大量的任務提交到後臺隊列時,某些任務會由於某些緣由(此處是 CGFont 鎖)被鎖住致使線程休眠,或者被阻塞,concurrent queue 隨後會建立新的線程來執行其餘任務。當這種狀況變多時,或者 App 中使用了大量 concurrent queue 來執行較多任務時,App 在同一時刻就會存在幾十個線程同時運行、建立、銷燬。CPU 是用時間片輪轉來實現線程併發的,儘管 concurrent queue 能控制線程的優先級,但當大量線程同時建立運行銷燬時,這些操做仍然會擠佔掉主線程的 CPU 資源。ASDK 有個 Feed 列表的 Demo:SocialAppLayout,當列表內 Cell 過多,而且很是快速的滑動時,界面仍然會出現少許卡頓,我謹慎的猜想可能與這個問題有關。
使用 concurrent queue 時不可避免會遇到這種問題,但使用 serial queue 又不能充分利用多核 CPU 的資源。我寫了一個簡單的工具 YYDispatchQueuePool,爲不一樣優先級建立和 CPU 數量相同的 serial queue,每次從 pool 中獲取 queue 時,會輪詢返回其中一個 queue。我把 App 內全部異步操做,包括圖像解碼、對象釋放、異步繪製等,都按優先級不一樣放入了全局的 serial queue 中執行,這樣儘可能避免了過多線程致使的性能問題。
SDWebImage 在這個 Demo 裏仍然會產生少許性能問題,而且有些地方不能知足個人需求,因此我本身實現了一個性能更高的圖片加載庫。在顯示簡單的單張圖片時,利用 UIView.layer.contents 就足夠了,不必使用 UIImageView 帶來額外的資源消耗,爲此我在 CALayer 上添加了 setImageWithURL 等方法。除此以外,我還把圖片解碼等操做經過 YYDispatchQueuePool 進行管理,控制了 App 總線程數量。
上面這些優化作完後,微博 Demo 已經很是流暢了,但在個人設想中,仍然有一些進一步優化的技巧,但限於時間和精力我並無實現,下面簡單列一下:
列表中有很多視覺元素並不須要觸摸事件,這些元素能夠用 ASDK 的圖層合成技術預先繪製爲一張圖。
再進一步減小每一個 Cell 內圖層的數量,用 CALayer 替換掉 UIView。
目前每一個 Cell 的類型都是相同的,但顯示的內容卻各部同樣,好比有的 Cell 有圖片,有的 Cell 裏是卡片。把 Cell 按類型劃分,進一步減小 Cell 內沒必要要的視圖對象和操做,應該能有一些效果。
把須要放到主線程執行的任務劃分爲足夠小的塊,並經過 Runloop 來進行調度,在每一個 Loop 裏判斷下一次 VSync 的時間,並在下次 VSync 到來前,把當前未執行完的任務延遲到下一個機會去。這個只是個人一個設想,並不必定能實現或起做用。
最後仍是要提一下,「過早的優化是萬惡之源」,在需求未定,性能問題不明顯時,不必嘗試作優化,而要儘可能正確的實現功能。作性能優化時,也最好是走修改代碼 -> Profile -> 修改代碼這樣一個流程,優先解決最值得優化的地方。
若是你須要一個明確的 FPS 指示器,能夠嘗試一下 KMCGeigerCounter。對於 CPU 的卡頓,它能夠經過內置的 CADisplayLink 檢測出來;對於 GPU 帶來的卡頓,它用了一個 1×1 的 SKView 來進行監視。這個項目有兩個小問題:SKView 雖然能監視到 GPU 的卡頓,但引入 SKView 自己就會對 CPU/GPU 帶來額外的一點的資源消耗;這個項目在 iOS 9 下有一些兼容問題,須要稍做調整。
我本身也寫了個簡單的 FPS 指示器:FPSLabel 只有幾十行代碼,僅用到了 CADisplayLink 來監視 CPU 的卡頓問題。雖然不如上面這個工具完善,但平常使用沒有太大問題。
最後,用 Instuments 的 GPU Driver 預設,可以實時查看到 CPU 和 GPU 的資源消耗。在這個預設內,你能查看到幾乎全部與顯示有關的數據,好比 Texture 數量、CA 提交的頻率、GPU 消耗等,在定位界面卡頓的問題時,這是最好的工具。
https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/#3