聲明:這篇文字轉自https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/
這篇文章會非常詳細的分析 iOS 界面構建中的各種性能問題以及對應的解決思路爪喘,同時給出一個開源的微博列表實現,通過實際的代碼展示如何構建流暢的交互贪染。
Index
演示項目
在開始技術討論前越走,你可以先下載我寫的 Demo 跑到真機上體驗一下:https://github.com/ibireme/YYKit棚品。 Demo 里包含一個微博的 Feed 列表靠欢、發(fā)布視圖,還包含一個 Twitter 的 Feed 列表铜跑。為了公平起見门怪,所有界面和交互我都從官方應用原封不動的抄了過來,數據也都是從官方應用抓取的锅纺。你也可以自己抓取數據替換掉 Demo 中的數據掷空,方便進行對比。盡管官方應用背后的功能更多更為復雜囤锉,但不至于會帶來太大的交互性能差異坦弟。
這個 Demo 最低可以運行在 iOS 6 上,所以你可以把它跑到老設備上體驗一下官地。在我的測試中酿傍,即使在 iPhone 4S 或者 iPad 3 上,Demo 列表在快速滑動時仍然能保持 50~60 FPS 的流暢交互区丑,而其他諸如微博拧粪、朋友圈等應用的列表視圖在滑動時已經有很嚴重的卡頓了。
微博的 Demo 有大約四千行代碼沧侥,Twitter 的只有兩千行左右代碼可霎,第三方庫只用到了 YYKit,文件數量比較少宴杀,方便查看癣朗。好了,下面是正文旺罢。
屏幕顯示圖像的原理
首先從過去的 CRT 顯示器原理說起旷余。CRT 的電子槍按照上面方式蒋歌,從上到下一行行掃描够坐,掃描完成后顯示器就呈現一幀畫面甜害,隨后電子槍回到初始位置繼續(xù)下一次掃描昧辽。為了把顯示器的顯示過程和系統的視頻控制器進行同步,顯示器(或者其他硬件)會用硬件時鐘產生一系列的定時信號卑雁。當電子槍換到新的一行跌榔,準備進行掃描時地熄,顯示器會發(fā)出一個水平同步信號(horizonal synchronization)叉讥,簡稱 HSync窘行;而當一幀畫面繪制完成后,電子槍回復到原位图仓,準備畫下一幀前罐盔,顯示器會發(fā)出一個垂直同步信號(vertical synchronization),簡稱 VSync救崔。顯示器通常以固定頻率進行刷新惶看,這個刷新率就是 VSync 信號產生的頻率捏顺。盡管現在的設備大都是液晶顯示屏了,但原理仍然沒有變纬黎。
通常來說草丧,計算機系統中 CPU、GPU莹桅、顯示器是以上面這種方式協同工作的。CPU 計算好顯示內容提交到 GPU烛亦,GPU 渲染完成后將渲染結果放入幀緩沖區(qū)诈泼,隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數據,經過可能的數模轉換傳遞給顯示器顯示煤禽。
在最簡單的情況下铐达,幀緩沖區(qū)只有一個,這時幀緩沖區(qū)的讀取和刷新都都會有比較大的效率問題檬果。為了解決效率問題瓮孙,顯示系統通常會引入兩個緩沖區(qū),即雙緩沖機制选脊。在這種情況下杭抠,GPU 會預先渲染好一幀放入一個緩沖區(qū)內,讓視頻控制器讀取恳啥,當下一幀渲染好后偏灿,GPU 會直接把視頻控制器的指針指向第二個緩沖器。如此一來效率會有很大的提升钝的。
雙緩沖雖然能解決效率問題翁垂,但會引入一個新的問題。當視頻控制器還未讀取完成時硝桩,即屏幕內容剛顯示一半時沿猜,GPU 將新的一幀內容提交到幀緩沖區(qū)并把兩個緩沖區(qū)進行交換后,視頻控制器就會把新的一幀數據的下半段顯示到屏幕上碗脊,造成畫面撕裂現象啼肩,如下圖:
為了解決這個問題,GPU 通常有一個機制叫做垂直同步(簡寫也是 V-Sync)望薄,當開啟垂直同步后疟游,GPU 會等待顯示器的 VSync 信號發(fā)出后,才進行新的一幀渲染和緩沖區(qū)更新痕支。這樣能解決畫面撕裂現象颁虐,也增加了畫面流暢度,但需要消費更多的計算資源卧须,也會帶來部分延遲另绩。
那么目前主流的移動設備是什么情況呢儒陨?從網上查到的資料可以知道,iOS 設備會始終使用雙緩存笋籽,并開啟垂直同步蹦漠。而安卓設備直到 4.1 版本,Google 才開始引入這種機制车海,目前安卓系統是三緩存+垂直同步笛园。
卡頓產生的原因和解決方案
在 VSync 信號到來后,系統圖形服務會通過 CADisplayLink 等機制通知 App侍芝,App 主線程開始在 CPU 中計算顯示內容研铆,比如視圖的創(chuàng)建、布局計算州叠、圖片解碼棵红、文本繪制等。隨后 CPU 會將計算好的內容提交到 GPU 去咧栗,由 GPU 進行變換逆甜、合成、渲染致板。隨后 GPU 會把渲染結果提交到幀緩沖區(qū)去交煞,等待下一次 VSync 信號到來時顯示到屏幕上。由于垂直同步的機制可岂,如果在一個 VSync 時間內错敢,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄缕粹,等待下一次機會再顯示稚茅,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因平斩。
從上面的圖中可以看到亚享,CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現象绘面。所以開發(fā)時欺税,也需要分別對 CPU 和 GPU 壓力進行評估和優(yōu)化。
CPU 資源消耗原因和解決方案
對象創(chuàng)建
對象的創(chuàng)建會分配內存揭璃、調整屬性晚凿、甚至還有讀取文件等操作,比較消耗 CPU 資源瘦馍。盡量用輕量的對象代替重量的對象歼秽,可以對性能有所優(yōu)化。比如 CALayer 比 UIView 要輕量許多情组,那么不需要響應觸摸事件的控件燥筷,用 CALayer 顯示會更加合適箩祥。如果對象不涉及 UI 操作,則盡量放到后臺線程去創(chuàng)建肆氓,但可惜的是包含有 CALayer 的控件袍祖,都只能在主線程創(chuàng)建和操作。通過 Storyboard 創(chuàng)建視圖對象時谢揪,其資源消耗會比直接通過代碼創(chuàng)建對象要大非常多蕉陋,在性能敏感的界面里,Storyboard 并不是一個好的技術選擇拨扶。
盡量推遲對象創(chuàng)建的時間寺滚,并把對象的創(chuàng)建分散到多個任務中去。盡管這實現起來比較麻煩屈雄,并且?guī)淼膬?yōu)勢并不多,但如果有能力做官套,還是要盡量嘗試一下酒奶。如果對象可以復用,并且復用的代價比釋放奶赔、創(chuàng)建新對象要小惋嚎,那么這類對象應當盡量放到一個緩存池里復用。
對象調整
對象的調整也經常是消耗 CPU 資源的地方站刑。這里特別說一下 CALayer:CALayer 內部并沒有屬性另伍,當調用屬性方法時,它內部是通過運行時?resolveInstanceMethod 為對象臨時添加一個方法绞旅,并把對應屬性值保存到內部的一個 Dictionary 里摆尝,同時還會通知 delegate、創(chuàng)建動畫等等因悲,非常消耗資源堕汞。UIView 的關于顯示相關的屬性(比如 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的,所以對 UIView 的這些屬性進行調整時晃琳,消耗的資源要遠大于一般的屬性讯检。對此你在應用中,應該盡量減少不必要的屬性修改卫旱。
當視圖層次調整時人灼,UIView、CALayer 之間會出現很多方法調用與通知顾翼,所以在優(yōu)化性能時投放,應該盡量避免調整視圖層次、添加和移除視圖暴构。
對象銷毀
對象的銷毀雖然消耗資源不多跪呈,但累積起來也是不容忽視的段磨。通常當容器類持有大量對象時,其銷毀時的資源消耗就非常明顯耗绿。同樣的苹支,如果對象可以放到后臺線程去釋放,那就挪到后臺線程去误阻。這里有個小 Tip:把對象捕獲到 block 中债蜜,然后扔到后臺隊列去隨便發(fā)送個消息以避免編譯器警告,就可以讓對象在后臺線程銷毀了究反。
1
2
3
4
5NSArray *tmp=self.array;
self.array=nil;
dispatch_async(queue,^{
[tmpclass];
});
布局計算
視圖布局的計算是 App 中最為常見的消耗 CPU 資源的地方寻定。如果能在后臺線程提前計算好視圖布局、并且對視圖布局進行緩存精耐,那么這個地方基本就不會產生性能問題了狼速。
不論通過何種技術對視圖進行布局,其最終都會落到對 UIView.frame/bounds/center 等屬性的調整上卦停。上面也說過向胡,對這些屬性的調整非常消耗資源,所以盡量提前計算好布局惊完,在需要時一次性調整好對應屬性僵芹,而不要多次、頻繁的計算和調整這些屬性小槐。
Autolayout
Autolayout 是蘋果本身提倡的技術拇派,在大部分情況下也能很好的提升開發(fā)效率,但是 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 對文本異步繪制桨啃。盡管這實現起來非常麻煩髓抑,但其帶來的優(yōu)勢也非常大,CoreText 對象創(chuàng)建好后优幸,能直接獲取文本的寬高等信息,避免了多次計算(調整 UILabel 大小時算一遍褪猛、UILabel 繪制時內部再算一遍)网杆;CoreText 對象占用內存較少,可以緩存下來以備稍后多次渲染伊滋。
圖片的解碼
當你用 UIImage 或 CGImageSource 的那幾個方法創(chuàng)建圖片時碳却,圖片數據并不會立刻解碼。圖片設置到 UIImageView 或者 CALayer.contents 中去笑旺,并且 CALayer 被提交到 GPU 前昼浦,CGImage 中的數據才會得到解碼。這一步是發(fā)生在主線程的筒主,并且不可避免关噪。如果想要繞開這個機制,常見的做法是在后臺線程先把圖片繪制到 CGBitmapContext 中乌妙,然后從 Bitmap 直接創(chuàng)建圖片使兔。目前常見的網絡圖片庫都自帶這個功能。
圖像的繪制
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中藤韵,然后從畫布創(chuàng)建圖片并顯示這樣一個過程虐沥。這個最常見的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是線程安全的,所以圖像的繪制可以很容易的放到后臺線程進行欲险。一個簡單異步繪制的過程大致如下(實際情況會比這個復雜得多镐依,但原理基本一致):
1
2
3
4
5
6
7
8
9
10
11-(void)display{
dispatch_async(backgroundQueue,^{
CGContextRefctx=CGBitmapContextCreate(...);
// draw in context...
CGImageRefimg=CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue,^{
layer.contents=img;
});
});
}
GPU 資源消耗原因和解決方案
相對于 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 的矢量圖形顯示讽坏,通常會觸發(fā)離屏渲染(offscreen rendering)锭魔,而離屏渲染通常發(fā)生在 GPU 中。當一個列表視圖中出現大量圓角的 CALayer路呜,并且快速滑動時迷捧,可以觀察到 GPU 資源已經占滿,而 CPU 資源消耗很少胀葱。這時界面仍然能正衬铮滑動,但平均幀數會降到很低抵屿。為了避免這種情況庆锦,可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉嫁到 CPU 上去轧葛。對于只需要圓角的某些場合搂抒,也可以用一張已經繪制好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果。最徹底的解決辦法朝群,就是把需要顯示的圖形在后臺線程繪制為圖片,避免使用圓角中符、陰影姜胖、遮罩等屬性。
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 開源的一個用于保持 iOS 界面流暢的庫淀散,我從中學到了很多東西右莱,所以下面我會花較大的篇幅來對其進行介紹和分析。
ASDK 的由來
ASDK 的作者是 Scott Goodson (Linkedin)档插,
他曾經在蘋果工作慢蜓,負責 iOS 的一些內置應用的開發(fā),比如股票郭膛、計算器晨抡、地圖、鐘表、設置耘柱、Safari 等如捅,當然他也參與了 UIKit framework 的開發(fā)。后來他加入 Facebook 后调煎,負責 Paper 的開發(fā)镜遣,創(chuàng)建并開源了 AsyncDisplayKit。目前他在 Pinterest 和 Instagram 負責 iOS 開發(fā)和用戶體驗的提升等工作士袄。
ASDK 自 2014 年 6 月開源悲关,10 月發(fā)布 1.0 版。目前 ASDK 即將要發(fā)布 2.0 版娄柳。
V2.0 增加了更多布局相關的代碼寓辱,ComponentKit 團隊為此貢獻很多。
現在 Github 的 master 分支上的版本是 V1.9.1西土,已經包含了 V2.0 的全部內容讶舰。
ASDK 的資料
想要了解 ASDK 的原理和細節(jié),最好從下面幾個視頻開始:
2014.10.15NSLondon – Scott Goodson – Behind AsyncDisplayKit
2015.03.02MCE 2015 – Scott Goodson – Effortless Responsiveness with AsyncDisplayKit
2015.10.25AsyncDisplayKit 2.0: Intelligent User Interfaces – NSSpain 2015
前兩個視頻內容大同小異需了,都是介紹 ASDK 的基本原理跳昼,附帶介紹 POP 等其他項目。
后一個視頻增加了 ASDK 2.0 的新特性的介紹肋乍。
除此之外鹅颊,還可以到 Github Issues 里看一下 ASDK 相關的討論,下面是幾個比較重要的內容:
關于 ComponentKit 和 ASDK 的區(qū)別
為什么不支持 Storyboard 和 Autolayout
之后墓造,還可以到 Google Groups 來查看和討論更多內容:
https://groups.google.com/forum/#!forum/asyncdisplaykit
ASDK 的基本原理
ASDK 認為堪伍,阻塞主線程的任務,主要分為上面這三大類觅闽。文本和布局的計算帝雇、渲染、解碼蛉拙、繪制都可以通過各種方式異步執(zhí)行尸闸,但 UIKit 和 Core Animation 相關操作必需在主線程進行。ASDK 的目標孕锄,就是盡量把這些任務從主線程挪走吮廉,而挪不走的,就盡量優(yōu)化性能畸肆。
為了達成這一目標宦芦,ASDK 嘗試對 UIKit 組件進行封裝:
這是常見的 UIView 和 CALayer 的關系:View 持有 Layer 用于顯示,View 中大部分顯示屬性實際是從 Layer 映射而來轴脐;Layer 的 delegate 在這里是 View调卑,當其屬性改變抡砂、動畫產生時,View 能夠得到通知令野。UIView 和 CALayer 不是線程安全的舀患,并且只能在主線程創(chuàng)建、訪問和銷毀气破。
ASDK 為此創(chuàng)建了 ASDisplayNode 類聊浅,包裝了常見的視圖屬性(比如 frame/bounds/alpha/transform/backgroundColor/superNode/subNodes 等),然后它用 UIView->CALayer 相同的方式现使,實現了 ASNode->UIView 這樣一個關系低匙。
當不需要響應觸摸事件時,ASDisplayNode 可以被設置為 layer backed碳锈,即 ASDisplayNode 充當了原來 UIView 的功能顽冶,節(jié)省了更多資源。
與 UIView 和 CALayer 不同售碳,ASDisplayNode 是線程安全的强重,它可以在后臺線程創(chuàng)建和修改。Node 剛創(chuàng)建時贸人,并不會在內部新建 UIView 和 CALayer间景,直到第一次在主線程訪問 view 或 layer 屬性時,它才會在內部生成對應的對象艺智。當它的屬性(比如frame/transform)改變后倘要,它并不會立刻同步到其持有的 view 或 layer 去,而是把被改變的屬性保存到內部的一個中間變量十拣,稍后在需要時封拧,再通過某個機制一次性設置到內部的 view 或 layer。
通過模擬和封裝 UIView/CALayer夭问,開發(fā)者可以把代碼中的 UIView 替換為 ASNode泽西,很大的降低了開發(fā)和學習成本,同時能獲得 ASDK 底層大量的性能優(yōu)化缰趋。為了方便使用捧杉, ASDK 把大量常用控件都封裝成了 ASNode 的子類,比如 Button埠胖、Control糠溜、Cell淳玩、Image直撤、ImageView、Text蜕着、TableView、CollectionView 等壮吩。利用這些控件磕蒲,開發(fā)者可以盡量避免直接使用 UIKit 相關控件,以獲得更完整的性能提升锤悄。
ASDK 的圖層預合成
有時一個 layer 會包含很多 sub-layer,而這些 sub-layer 并不需要響應觸摸事件嘉抒,也不需要進行動畫和位置調整零聚。ASDK 為此實現了一個被稱為 pre-composing 的技術,可以把這些 sub-layer 合成渲染為一張圖片些侍。開發(fā)時隶症,ASNode 已經替代了 UIView 和 CALayer;直接使用各種 Node 控件并設置為 layer backed 后岗宣,ASNode 甚至可以通過預合成來避免創(chuàng)建內部的 UIView 和 CALayer蚂会。
通過這種方式,把一個大的層級耗式,通過一個大的繪制方法繪制到一張圖上胁住,性能會獲得很大提升。CPU 避免了創(chuàng)建 UIKit 對象的資源消耗刊咳,GPU 避免了多張 texture 合成和渲染的消耗彪见,更少的 bitmap 也意味著更少的內存占用。
ASDK 異步并發(fā)操作
自 iPhone 4S 起芦缰,iDevice 已經都是雙核 CPU 了企巢,現在的 iPad 甚至已經更新到 3 核了。充分利用多核的優(yōu)勢让蕾、并發(fā)執(zhí)行任務對保持界面流暢有很大作用浪规。ASDK 把布局計算、文本排版探孝、圖片/文本/圖形渲染等操作都封裝成較小的任務笋婿,并利用 GCD 異步并發(fā)執(zhí)行。如果開發(fā)者使用了 ASNode 相關的控件顿颅,那么這些并發(fā)操作會自動在后臺進行缸濒,無需進行過多配置。
Runloop 任務分發(fā)
Runloop work distribution 是 ASDK 比較核心的一個技術粱腻,ASDK 的介紹視頻和文檔中都沒有詳細展開介紹庇配,所以這里我會多做一些分析。如果你對 Runloop 還不太了解绍些,可以看一下我之前的文章深入理解RunLoop捞慌,里面對 ASDK 也有所提及。
iOS 的顯示系統是由 VSync 信號驅動的柬批,VSync 信號由硬件時鐘生成啸澡,每秒鐘發(fā)出 60 次(這個值取決設備硬件袖订,比如 iPhone 真機上通常是 59.97)。iOS 圖形服務接收到 VSync 信號后嗅虏,會通過 IPC 通知到 App 內洛姑。App 的 Runloop 在啟動后會注冊對應的 CFRunLoopSource 通過 mach_port 接收傳過來的時鐘信號通知,隨后 Source 的回調會驅動整個 App 的動畫與顯示皮服。
Core Animation 在 RunLoop 中注冊了一個 Observer楞艾,監(jiān)聽了 BeforeWaiting 和 Exit 事件。這個 Observer 的優(yōu)先級是 2000000龄广,低于常見的其他 Observer产徊。當一個觸摸事件到來時,RunLoop 被喚醒蜀细,App 中的代碼會執(zhí)行一些操作舟铜,比如創(chuàng)建和調整視圖層級、設置 UIView 的 frame奠衔、修改 CALayer 的透明度谆刨、為視圖添加一個動畫;這些操作最終都會被 CALayer 捕獲归斤,并通過 CATransaction 提交到一個中間狀態(tài)去(CATransaction 的文檔略有提到這些內容痊夭,但并不完整)。當上面所有操作結束后脏里,RunLoop 即將進入休眠(或者退出)時她我,關注該事件的 Observer 都會得到通知。這時 CA 注冊的那個 Observer 就會在回調中迫横,把所有的中間狀態(tài)合并提交到 GPU 去顯示番舆;如果此處有動畫,CA 會通過 DisplayLink 等機制多次觸發(fā)相關流程矾踱。
ASDK 在此處模擬了 Core Animation 的這個機制:所有針對 ASNode 的修改和提交恨狈,總有些任務是必需放入主線程執(zhí)行的。當出現這種任務時呛讲,ASNode 會把任務用 ASAsyncTransaction(Group) 封裝并提交到一個全局的容器去禾怠。ASDK 也在 RunLoop 中注冊了一個 Observer,監(jiān)視的事件和 CA 一樣贝搁,但優(yōu)先級比 CA 要低吗氏。當 RunLoop 進入休眠前、CA 處理完事件后雷逆,ASDK 就會執(zhí)行該 loop 內提交的所有任務弦讽。具體代碼見這個文件:ASAsyncTransactionGroup。
通過這種機制关面,ASDK 可以在合適的機會把異步坦袍、并發(fā)的操作同步到主線程去,并且能獲得不錯的性能等太。
其他
ASDK 中還有封裝很多高級的功能捂齐,比如滑動列表的預加載、V2.0添加的新的布局模式等缩抡。ASDK 是一個很龐大的庫奠宜,它本身并不推薦你把整個 App 全部都改為 ASDK 驅動,把最需要提升交互性能的地方用 ASDK 進行優(yōu)化就足夠了瞻想。
微博 Demo 性能優(yōu)化技巧
我為了演示 YYKit 的功能压真,實現了微博和 Twitter 的 Demo,并為它們做了不少性能優(yōu)化蘑险,下面就是優(yōu)化時用到的一些技巧滴肿。
預排版
當獲取到 API JSON 數據后,我會把每條 Cell 需要的數據都在后臺線程計算并封裝為一個布局對象 CellLayout佃迄。CellLayout 包含所有文本的 CoreText 排版結果泼差、Cell 內部每個控件的高度、Cell 的整體高度呵俏。每個 CellLayout 的內存占用并不多堆缘,所以當生成后,可以全部緩存到內存普碎,以供稍后使用吼肥。這樣,TableView 在請求各個高度函數時麻车,不會消耗任何多余計算量缀皱;當把 CellLayout 設置到 Cell 內部時,Cell 內部也不用再計算布局了动猬。
對于通常的 TableView 來說唆鸡,提前在后臺計算好布局結果是非常重要的一個性能優(yōu)化點。為了達到最高性能枣察,你可能需要犧牲一些開發(fā)速度争占,不要用 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 快速滑動時逆瑞,會有大量異步繪制任務提交到后臺線程去執(zhí)行荠藤。但是有時滑動速度過快時,繪制任務還沒有完成就可能已經被取消了获高。如果這時仍然繼續(xù)繪制哈肖,就會造成大量的 CPU 資源浪費,甚至阻塞線程并造成后續(xù)的繪制任務遲遲無法完成念秧。我的做法是盡量快速淤井、提前判斷當前繪制任務是否已經被取消;在繪制每一行文本前,我都會調用 isCancelled() 來進行判斷币狠,保證被取消的任務能及時退出游两,不至于影響后續(xù)操作。
目前有些第三方微博客戶端(比如 VVebo漩绵、墨客等)贱案,使用了一種方式來避免高速滑動時 Cell 的繪制過程,相關實現見這個項目:VVeboTableViewDemo渐行。它的原理是,當滑動時铸董,松開手指后祟印,立刻計算出滑動停止時 Cell 的位置,并預先繪制那個位置附近的幾個 Cell粟害,而忽略當前滑動中的 Cell蕴忆。這個方法比較有技巧性,并且對于滑動性能來說提升也很大悲幅,唯一的缺點就是快速滑動中會出現大量空白內容套鹅。如果你不想實現比較麻煩的異步繪制但又想保證滑動的流暢性,這個技巧是個不錯的選擇汰具。
全局并發(fā)控制
當我用 concurrent queue 來執(zhí)行大量繪制任務時卓鹿,偶爾會遇到這種問題:
大量的任務提交到后臺隊列時,某些任務會因為某些原因(此處是 CGFont 鎖)被鎖住導致線程休眠留荔,或者被阻塞吟孙,concurrent queue 隨后會創(chuàng)建新的線程來執(zhí)行其他任務。當這種情況變多時聚蝶,或者 App 中使用了大量 concurrent queue 來執(zhí)行較多任務時杰妓,App 在同一時刻就會存在幾十個線程同時運行、創(chuàng)建碘勉、銷毀巷挥。CPU 是用時間片輪轉來實現線程并發(fā)的,盡管 concurrent queue 能控制線程的優(yōu)先級验靡,但當大量線程同時創(chuàng)建運行銷毀時倍宾,這些操作仍然會擠占掉主線程的 CPU 資源。ASDK 有個 Feed 列表的 Demo:SocialAppLayout胜嗓,當列表內 Cell 過多凿宾,并且非常快速的滑動時兼蕊,界面仍然會出現少量卡頓初厚,我謹慎的猜測可能與這個問題有關。
使用 concurrent queue 時不可避免會遇到這種問題,但使用 serial queue 又不能充分利用多核 CPU 的資源产禾。我寫了一個簡單的工具YYDispatchQueuePool排作,為不同優(yōu)先級創(chuàng)建和 CPU 數量相同的 serial queue,每次從 pool 中獲取 queue 時亚情,會輪詢返回其中一個 queue妄痪。我把 App 內所有異步操作,包括圖像解碼楞件、對象釋放衫生、異步繪制等,都按優(yōu)先級不同放入了全局的 serial queue 中執(zhí)行土浸,這樣盡量避免了過多線程導致的性能問題罪针。
更高效的異步圖片加載
SDWebImage 在這個 Demo 里仍然會產生少量性能問題,并且有些地方不能滿足我的需求黄伊,所以我自己實現了一個性能更高的圖片加載庫泪酱。在顯示簡單的單張圖片時,利用 UIView.layer.contents 就足夠了还最,沒必要使用 UIImageView 帶來額外的資源消耗墓阀,為此我在 CALayer 上添加了 setImageWithURL 等方法。除此之外拓轻,我還把圖片解碼等操作通過 YYDispatchQueuePool 進行管理斯撮,控制了 App 總線程數量。
其他可以改進的地方
上面這些優(yōu)化做完后扶叉,微博 Demo 已經非常流暢了吮成,但在我的設想中,仍然有一些進一步優(yōu)化的技巧辜梳,但限于時間和精力我并沒有實現粱甫,下面簡單列一下:
列表中有不少視覺元素并不需要觸摸事件,這些元素可以用 ASDK 的圖層合成技術預先繪制為一張圖作瞄。
再進一步減少每個 Cell 內圖層的數量茶宵,用 CALayer 替換掉 UIView。
目前每個 Cell 的類型都是相同的宗挥,但顯示的內容卻各部一樣乌庶,比如有的 Cell 有圖片,有的 Cell 里是卡片契耿。把 Cell 按類型劃分瞒大,進一步減少 Cell 內不必要的視圖對象和操作,應該能有一些效果搪桂。
把需要放到主線程執(zhí)行的任務劃分為足夠小的塊透敌,并通過 Runloop 來進行調度盯滚,在每個 Loop 里判斷下一次 VSync 的時間,并在下次 VSync 到來前酗电,把當前未執(zhí)行完的任務延遲到下一個機會去魄藕。這個只是我的一個設想,并不一定能實現或起作用撵术。
如何評測界面的流暢度
最后還是要提一下背率,“過早的優(yōu)化是萬惡之源”,在需求未定嫩与,性能問題不明顯時寝姿,沒必要嘗試做優(yōu)化,而要盡量正確的實現功能划滋。做性能優(yōu)化時饵筑,也最好是走修改代碼 -> Profile -> 修改代碼這樣一個流程,優(yōu)先解決最值得優(yōu)化的地方古毛。
如果你需要一個明確的 FPS 指示器翻翩,可以嘗試一下KMCGeigerCounter都许。對于 CPU 的卡頓稻薇,它可以通過內置的 CADisplayLink 檢測出來;對于 GPU 帶來的卡頓胶征,它用了一個 1×1 的 SKView 來進行監(jiān)視塞椎。這個項目有兩個小問題:SKView 雖然能監(jiān)視到 GPU 的卡頓,但引入 SKView 本身就會對 CPU/GPU 帶來額外的一點的資源消耗睛低;這個項目在 iOS 9 下有一些兼容問題案狠,需要稍作調整。
我自己也寫了個簡單的 FPS 指示器:FPSLabel只有幾十行代碼钱雷,僅用到了 CADisplayLink 來監(jiān)視 CPU 的卡頓問題骂铁。雖然不如上面這個工具完善,但日常使用沒有太大問題罩抗。
最后拉庵,用 Instuments 的 GPU Driver 預設,能夠實時查看到 CPU 和 GPU 的資源消耗套蒂。在這個預設內钞支,你能查看到幾乎所有與顯示有關的數據,比如 Texture 數量操刀、CA 提交的頻率烁挟、GPU 消耗等,在定位界面卡頓的問題時骨坑,這是最好的工具撼嗓。