轉(zhuǎn)載自:iOS 保持界面流暢的技巧(作者:ibireme)
這篇文章會非常詳細(xì)的分析 iOS 界面構(gòu)建中的各種性能問題以及對應(yīng)的解決思路,同時給出一個開源的微博列表實現(xiàn)牲芋,通過實際的代碼展示如何構(gòu)建流暢的交互并思。
Index
[演示項目]
[屏幕顯示圖像的原理]
[卡頓產(chǎn)生的原因和解決方案]
[CPU 資源消耗原因和解決方案]
[GPU 資源消耗原因和解決方案]
[AsyncDisplayKit]
[ASDK 的由來]
[ASDK 的資料]
[ASDK 的基本原理]
[ASDK 的圖層預(yù)合成]
[ASDK 異步并發(fā)操作]
[Runloop 任務(wù)分發(fā)]
[微博 Demo 性能優(yōu)化技巧]
[預(yù)排版]
[預(yù)渲染]
[異步繪制]
[全局并發(fā)控制]
[更高效的異步圖片加載]
[其他可以改進(jìn)的地方]
[如何評測界面的流暢度]
演示項目
在開始技術(shù)討論前蕊肥,你可以先下載我寫的 Demo 跑到真機(jī)上體驗一下:https://github.com/ibireme/YYKit薪铜。 Demo 里包含一個微博的 Feed 列表、發(fā)布視圖容为,還包含一個 Twitter 的 Feed 列表乓序。為了公平起見,所有界面和交互我都從官方應(yīng)用原封不動的抄了過來坎背,數(shù)據(jù)也都是從官方應(yīng)用抓取的竭缝。你也可以自己抓取數(shù)據(jù)替換掉 Demo 中的數(shù)據(jù),方便進(jìn)行對比沼瘫。盡管官方應(yīng)用背后的功能更多更為復(fù)雜,但不至于會帶來太大的交互性能差異咙俩。
這個 Demo 最低可以運行在 iOS 6 上耿戚,所以你可以把它跑到老設(shè)備上體驗一下湿故。在我的測試中,即使在 iPhone 4S 或者 iPad 3 上膜蛔,Demo 列表在快速滑動時仍然能保持 50~60 FPS 的流暢交互坛猪,而其他諸如微博、朋友圈等應(yīng)用的列表視圖在滑動時已經(jīng)有很嚴(yán)重的卡頓了皂股。
微博的 Demo 有大約四千行代碼墅茉,Twitter 的只有兩千行左右代碼,第三方庫只用到了 YYKit呜呐,文件數(shù)量比較少就斤,方便查看。好了蘑辑,下面是正文洋机。
屏幕顯示圖像的原理
首先從過去的 CRT 顯示器原理說起。CRT 的電子槍按照上面方式洋魂,從上到下一行行掃描绷旗,掃描完成后顯示器就呈現(xiàn)一幀畫面,隨后電子槍回到初始位置繼續(xù)下一次掃描副砍。為了把顯示器的顯示過程和系統(tǒng)的視頻控制器進(jìn)行同步衔肢,顯示器(或者其他硬件)會用硬件時鐘產(chǎn)生一系列的定時信號。當(dāng)電子槍換到新的一行豁翎,準(zhǔn)備進(jìn)行掃描時角骤,顯示器會發(fā)出一個水平同步信號(horizonal synchronization),簡稱 HSync谨垃;而當(dāng)一幀畫面繪制完成后启搂,電子槍回復(fù)到原位,準(zhǔn)備畫下一幀前刘陶,顯示器會發(fā)出一個垂直同步信號(vertical synchronization)胳赌,簡稱 VSync。顯示器通常以固定頻率進(jìn)行刷新匙隔,這個刷新率就是 VSync 信號產(chǎn)生的頻率疑苫。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏了,但原理仍然沒有變纷责。
通常來說捍掺,計算機(jī)系統(tǒng)中 CPU、GPU再膳、顯示器是以上面這種方式協(xié)同工作的挺勿。CPU 計算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū)喂柒,隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù)不瓶,經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示禾嫉。
在最簡單的情況下,幀緩沖區(qū)只有一個蚊丐,這時幀緩沖區(qū)的讀取和刷新都都會有比較大的效率問題熙参。為了解決效率問題,顯示系統(tǒng)通常會引入兩個緩沖區(qū)麦备,即雙緩沖機(jī)制孽椰。在這種情況下,GPU 會預(yù)先渲染好一幀放入一個緩沖區(qū)內(nèi)凛篙,讓視頻控制器讀取黍匾,當(dāng)下一幀渲染好后,GPU 會直接把視頻控制器的指針指向第二個緩沖器鞋诗。如此一來效率會有很大的提升膀捷。
雙緩沖雖然能解決效率問題,但會引入一個新的問題削彬。當(dāng)視頻控制器還未讀取完成時全庸,即屏幕內(nèi)容剛顯示一半時,GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個緩沖區(qū)進(jìn)行交換后融痛,視頻控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上壶笼,造成畫面撕裂現(xiàn)象,如下圖:
為了解決這個問題雁刷,GPU 通常有一個機(jī)制叫做垂直同步(簡寫也是 V-Sync)覆劈,當(dāng)開啟垂直同步后,GPU 會等待顯示器的 VSync 信號發(fā)出后沛励,才進(jìn)行新的一幀渲染和緩沖區(qū)更新责语。這樣能解決畫面撕裂現(xiàn)象,也增加了畫面流暢度目派,但需要消費更多的計算資源煮纵,也會帶來部分延遲夏哭。
那么目前主流的移動設(shè)備是什么情況呢是己?從網(wǎng)上查到的資料可以知道挖诸,iOS 設(shè)備會始終使用雙緩存,并開啟垂直同步谅摄。而安卓設(shè)備直到 4.1 版本徒河,Google 才開始引入這種機(jī)制,目前安卓系統(tǒng)是三緩存+垂直同步送漠。
卡頓產(chǎn)生的原因和解決方案
在 VSync 信號到來后顽照,系統(tǒng)圖形服務(wù)會通過 CADisplayLink 等機(jī)制通知 App,App 主線程開始在 CPU 中計算顯示內(nèi)容闽寡,比如視圖的創(chuàng)建代兵、布局計算纵穿、圖片解碼、文本繪制等奢人。隨后 CPU 會將計算好的內(nèi)容提交到 GPU 去,由 GPU 進(jìn)行變換淆院、合成何乎、渲染。隨后 GPU 會把渲染結(jié)果提交到幀緩沖區(qū)去土辩,等待下一次 VSync 信號到來時顯示到屏幕上支救。由于垂直同步的機(jī)制,如果在一個 VSync 時間內(nèi)拷淘,CPU 或者 GPU 沒有完成內(nèi)容提交各墨,則那一幀就會被丟棄,等待下一次機(jī)會再顯示启涯,而這時顯示屏?xí)A糁暗膬?nèi)容不變贬堵。這就是界面卡頓的原因。
從上面的圖中可以看到结洼,CPU 和 GPU 不論哪個阻礙了顯示流程黎做,都會造成掉幀現(xiàn)象。所以開發(fā)時松忍,也需要分別對 CPU 和 GPU 壓力進(jìn)行評估和優(yōu)化蒸殿。
CPU 資源消耗原因和解決方案
對象創(chuàng)建
對象的創(chuàng)建會分配內(nèi)存、調(diào)整屬性鸣峭、甚至還有讀取文件等操作宏所,比較消耗 CPU 資源。盡量用輕量的對象代替重量的對象摊溶,可以對性能有所優(yōu)化爬骤。比如 CALayer 比 UIView 要輕量許多,那么不需要響應(yīng)觸摸事件的控件更扁,用 CALayer 顯示會更加合適盖腕。如果對象不涉及 UI 操作,則盡量放到后臺線程去創(chuàng)建浓镜,但可惜的是包含有 CALayer 的控件溃列,都只能在主線程創(chuàng)建和操作。通過 Storyboard 創(chuàng)建視圖對象時膛薛,其資源消耗會比直接通過代碼創(chuàng)建對象要大非常多听隐,在性能敏感的界面里,Storyboard 并不是一個好的技術(shù)選擇哄啄。
盡量推遲對象創(chuàng)建的時間雅任,并把對象的創(chuàng)建分散到多個任務(wù)中去风范。盡管這實現(xiàn)起來比較麻煩,并且?guī)淼膬?yōu)勢并不多沪么,但如果有能力做硼婿,還是要盡量嘗試一下。如果對象可以復(fù)用禽车,并且復(fù)用的代價比釋放寇漫、創(chuàng)建新對象要小,那么這類對象應(yīng)當(dāng)盡量放到一個緩存池里復(fù)用殉摔。
對象調(diào)整
對象的調(diào)整也經(jīng)常是消耗 CPU 資源的地方州胳。這里特別說一下 CALayer:CALayer 內(nèi)部并沒有屬性,當(dāng)調(diào)用屬性方法時逸月,它內(nèi)部是通過運行時 resolveInstanceMethod 為對象臨時添加一個方法栓撞,并把對應(yīng)屬性值保存到內(nèi)部的一個 Dictionary 里,同時還會通知 delegate碗硬、創(chuàng)建動畫等等瓤湘,非常消耗資源。UIView 的關(guān)于顯示相關(guān)的屬性(比如 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的肛响,所以對 UIView 的這些屬性進(jìn)行調(diào)整時岭粤,消耗的資源要遠(yuǎn)大于一般的屬性。對此你在應(yīng)用中特笋,應(yīng)該盡量減少不必要的屬性修改剃浇。
當(dāng)視圖層次調(diào)整時,UIView猎物、CALayer 之間會出現(xiàn)很多方法調(diào)用與通知虎囚,所以在優(yōu)化性能時,應(yīng)該盡量避免調(diào)整視圖層次蔫磨、添加和移除視圖淘讥。
對象銷毀
對象的銷毀雖然消耗資源不多,但累積起來也是不容忽視的堤如。通常當(dāng)容器類持有大量對象時蒲列,其銷毀時的資源消耗就非常明顯。同樣的搀罢,如果對象可以放到后臺線程去釋放蝗岖,那就挪到后臺線程去。這里有個小 Tip:把對象捕獲到 block 中榔至,然后扔到后臺隊列去隨便發(fā)送個消息以避免編譯器警告抵赢,就可以讓對象在后臺線程銷毀了。
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
布局計算
視圖布局的計算是 App 中最為常見的消耗 CPU 資源的地方。如果能在后臺線程提前計算好視圖布局铅鲤、并且對視圖布局進(jìn)行緩存划提,那么這個地方基本就不會產(chǎn)生性能問題了。
不論通過何種技術(shù)對視圖進(jìn)行布局邢享,其最終都會落到對 UIView.frame/bounds/center 等屬性的調(diào)整上鹏往。上面也說過,對這些屬性的調(diào)整非常消耗資源骇塘,所以盡量提前計算好布局掸犬,在需要時一次性調(diào)整好對應(yīng)屬性,而不要多次绪爸、頻繁的計算和調(diào)整這些屬性。
Autolayout
Autolayout 是蘋果本身提倡的技術(shù)宙攻,在大部分情況下也能很好的提升開發(fā)效率奠货,但是 Autolayout 對于復(fù)雜視圖來說常常會產(chǎn)生嚴(yán)重的性能問題。隨著視圖數(shù)量的增長座掘,Autolayout 帶來的 CPU 消耗會呈指數(shù)級上升递惋。具體數(shù)據(jù)可以看這個文章:http://pilky.me/36/。 如果你不想手動調(diào)整 frame 等屬性溢陪,你可以用一些工具方法替代(比如常見的 left/right/top/bottom/width/height 快捷屬性)萍虽,或者使用 ComponentKit、AsyncDisplayKit 等框架形真。
文本計算
如果一個界面中包含大量文本(比如微博微信朋友圈等)杉编,文本的寬高計算會占用很大一部分資源,并且不可避免咆霜。如果你對文本顯示沒有特殊要求邓馒,可以參考下 UILabel 內(nèi)部的實現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪制文本蛾坯。盡管這兩個方法性能不錯光酣,但仍舊需要放到后臺線程進(jìn)行以避免阻塞主線程。
如果你用 CoreText 繪制文本脉课,那就可以先生成 CoreText 排版對象救军,然后自己計算了,并且 CoreText 對象還能保留以供稍后繪制使用倘零。
文本渲染
屏幕上能看到的所有文本內(nèi)容控件唱遭,包括 UIWebView,在底層都是通過 CoreText 排版视事、繪制為 Bitmap 顯示的胆萧。常見的文本控件 (UILabel、UITextView 等),其排版和繪制都是在主線程進(jìn)行的跌穗,當(dāng)顯示大量文本時订晌,CPU 的壓力會非常大。對此解決方案只有一個蚌吸,那就是自定義文本控件锈拨,用 TextKit 或最底層的 CoreText 對文本異步繪制。盡管這實現(xiàn)起來非常麻煩羹唠,但其帶來的優(yōu)勢也非常大奕枢,CoreText 對象創(chuàng)建好后,能直接獲取文本的寬高等信息佩微,避免了多次計算(調(diào)整 UILabel 大小時算一遍缝彬、UILabel 繪制時內(nèi)部再算一遍);CoreText 對象占用內(nèi)存較少哺眯,可以緩存下來以備稍后多次渲染谷浅。
圖片的解碼
當(dāng)你用 UIImage 或 CGImageSource 的那幾個方法創(chuàng)建圖片時,圖片數(shù)據(jù)并不會立刻解碼奶卓。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去一疯,并且 CALayer 被提交到 GPU 前,CGImage 中的數(shù)據(jù)才會得到解碼夺姑。這一步是發(fā)生在主線程的墩邀,并且不可避免。如果想要繞開這個機(jī)制盏浙,常見的做法是在后臺線程先把圖片繪制到 CGBitmapContext 中眉睹,然后從 Bitmap 直接創(chuàng)建圖片。目前常見的網(wǎng)絡(luò)圖片庫都自帶這個功能废膘。
圖像的繪制
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中辣往,然后從畫布創(chuàng)建圖片并顯示這樣一個過程。這個最常見的地方就是 [UIView drawRect:] 里面了殖卑。由于 CoreGraphic 方法通常都是線程安全的站削,所以圖像的繪制可以很容易的放到后臺線程進(jìn)行。一個簡單異步繪制的過程大致如下(實際情況會比這個復(fù)雜得多孵稽,但原理基本一致):
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
GPU 資源消耗原因和解決方案
相對于 CPU 來說许起,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應(yīng)用變換(transform)菩鲜、混合并渲染园细,然后輸出到屏幕上。通常你所能看到的內(nèi)容接校,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類猛频。
紋理的渲染
所有的 Bitmap狮崩,包括圖片、文本鹿寻、柵格化的內(nèi)容睦柴,最終都要由內(nèi)存提交到顯存,綁定為 GPU Texture毡熏。不論是提交到顯存的過程坦敌,還是 GPU 調(diào)整和渲染 Texture 的過程,都要消耗不少 GPU 資源痢法。當(dāng)在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片并且快速滑動時)狱窘,CPU 占用率很低,GPU 占用非常高财搁,界面仍然會掉幀蘸炸。避免這種情況的方法只能是盡量減少在短時間內(nèi)大量圖片的顯示,盡可能將多張圖片合成為一張進(jìn)行顯示尖奔。
當(dāng)圖片過大幻馁,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進(jìn)行預(yù)處理越锈,這對 CPU 和 GPU 都會帶來額外的資源消耗。目前來說膘滨,iPhone 4S 以上機(jī)型甘凭,紋理尺寸上限都是 4096×4096,更詳細(xì)的資料可以看這里:iosres.com火邓。所以丹弱,盡量不要讓圖片和視圖的大小超過這個值。
視圖的混合 (Composing)
當(dāng)多個視圖(或者說 CALayer)重疊在一起顯示時铲咨,GPU 會首先把他們混合到一起躲胳。如果視圖結(jié)構(gòu)過于復(fù)雜,混合的過程也會消耗很多 GPU 資源纤勒。為了減輕這種情況的 GPU 消耗坯苹,應(yīng)用應(yīng)當(dāng)盡量減少視圖數(shù)量和層次,并在不透明的視圖里標(biāo)明 opaque 屬性以避免無用的 Alpha 通道合成摇天。當(dāng)然粹湃,這也可以用上面的方法,把多個視圖預(yù)先渲染為一張圖片來顯示泉坐。
圖形的生成为鳄。
CALayer 的 border、圓角腕让、陰影孤钦、遮罩(mask),CASharpLayer 的矢量圖形顯示,通常會觸發(fā)離屏渲染(offscreen rendering)偏形,而離屏渲染通常發(fā)生在 GPU 中静袖。當(dāng)一個列表視圖中出現(xiàn)大量圓角的 CALayer,并且快速滑動時壳猜,可以觀察到 GPU 資源已經(jīng)占滿勾徽,而 CPU 資源消耗很少。這時界面仍然能正惩嘲猓滑動喘帚,但平均幀數(shù)會降到很低。為了避免這種情況咒钟,可以嘗試開啟 CALayer.shouldRasterize 屬性吹由,但這會把原本離屏渲染的操作轉(zhuǎn)嫁到 CPU 上去。對于只需要圓角的某些場合朱嘴,也可以用一張已經(jīng)繪制好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果倾鲫。最徹底的解決辦法,就是把需要顯示的圖形在后臺線程繪制為圖片萍嬉,避免使用圓角乌昔、陰影、遮罩等屬性壤追。
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 開源的一個用于保持 iOS 界面流暢的庫磕道,我從中學(xué)到了很多東西,所以下面我會花較大的篇幅來對其進(jìn)行介紹和分析行冰。
ASDK 的由來
ASDK 的作者是 Scott Goodson (Linkedin)溺蕉,
他曾經(jīng)在蘋果工作,負(fù)責(zé) iOS 的一些內(nèi)置應(yīng)用的開發(fā)悼做,比如股票疯特、計算器、地圖肛走、鐘表漓雅、設(shè)置、Safari 等朽色,當(dāng)然他也參與了 UIKit framework 的開發(fā)故硅。后來他加入 Facebook 后,負(fù)責(zé) Paper 的開發(fā)纵搁,創(chuàng)建并開源了 AsyncDisplayKit吃衅。目前他在 Pinterest 和 Instagram 負(fù)責(zé) iOS 開發(fā)和用戶體驗的提升等工作。
ASDK 自 2014 年 6 月開源腾誉,10 月發(fā)布 1.0 版徘层。目前 ASDK 即將要發(fā)布 2.0 版峻呕。
V2.0 增加了更多布局相關(guān)的代碼,ComponentKit 團(tuán)隊為此貢獻(xiàn)很多趣效。
現(xiàn)在 Github 的 master 分支上的版本是 V1.9.1瘦癌,已經(jīng)包含了 V2.0 的全部內(nèi)容。
ASDK 的資料
想要了解 ASDK 的原理和細(xì)節(jié)跷敬,最好從下面幾個視頻開始:
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
前兩個視頻內(nèi)容大同小異讯私,都是介紹 ASDK 的基本原理,附帶介紹 POP 等其他項目西傀。
后一個視頻增加了 ASDK 2.0 的新特性的介紹斤寇。
除此之外,還可以到 Github Issues 里看一下 ASDK 相關(guān)的討論拥褂,下面是幾個比較重要的內(nèi)容:
關(guān)于 Runloop Dispatch
關(guān)于 ComponentKit 和 ASDK 的區(qū)別
為什么不支持 Storyboard 和 Autolayout
如何評測界面的流暢度
之后娘锁,還可以到 Google Groups 來查看和討論更多內(nèi)容:
https://groups.google.com/forum/#!forum/asyncdisplaykit
ASDK 的基本原理
ASDK 認(rèn)為,阻塞主線程的任務(wù)饺鹃,主要分為上面這三大類莫秆。文本和布局的計算、渲染悔详、解碼镊屎、繪制都可以通過各種方式異步執(zhí)行,但 UIKit 和 Core Animation 相關(guān)操作必需在主線程進(jìn)行茄螃。ASDK 的目標(biāo)缝驳,就是盡量把這些任務(wù)從主線程挪走,而挪不走的责蝠,就盡量優(yōu)化性能。
為了達(dá)成這一目標(biāo)萎庭,ASDK 嘗試對 UIKit 組件進(jìn)行封裝:
這是常見的 UIView 和 CALayer 的關(guān)系:View 持有 Layer 用于顯示霜医,View 中大部分顯示屬性實際是從 Layer 映射而來;Layer 的 delegate 在這里是 View驳规,當(dāng)其屬性改變肴敛、動畫產(chǎn)生時,View 能夠得到通知吗购。UIView 和 CALayer 不是線程安全的医男,并且只能在主線程創(chuàng)建、訪問和銷毀捻勉。
ASDK 為此創(chuàng)建了 ASDisplayNode 類镀梭,包裝了常見的視圖屬性(比如 frame/bounds/alpha/transform/backgroundColor/superNode/subNodes 等),然后它用 UIView->CALayer 相同的方式踱启,實現(xiàn)了 ASNode->UIView 這樣一個關(guān)系报账。
當(dāng)不需要響應(yīng)觸摸事件時研底,ASDisplayNode 可以被設(shè)置為 layer backed,即 ASDisplayNode 充當(dāng)了原來 UIView 的功能透罢,節(jié)省了更多資源榜晦。
與 UIView 和 CALayer 不同,ASDisplayNode 是線程安全的羽圃,它可以在后臺線程創(chuàng)建和修改乾胶。Node 剛創(chuàng)建時,并不會在內(nèi)部新建 UIView 和 CALayer朽寞,直到第一次在主線程訪問 view 或 layer 屬性時识窿,它才會在內(nèi)部生成對應(yīng)的對象。當(dāng)它的屬性(比如frame/transform)改變后愁憔,它并不會立刻同步到其持有的 view 或 layer 去腕扶,而是把被改變的屬性保存到內(nèi)部的一個中間變量,稍后在需要時吨掌,再通過某個機(jī)制一次性設(shè)置到內(nèi)部的 view 或 layer半抱。
通過模擬和封裝 UIView/CALayer,開發(fā)者可以把代碼中的 UIView 替換為 ASNode膜宋,很大的降低了開發(fā)和學(xué)習(xí)成本窿侈,同時能獲得 ASDK 底層大量的性能優(yōu)化。為了方便使用秋茫, ASDK 把大量常用控件都封裝成了 ASNode 的子類史简,比如 Button、Control肛著、Cell圆兵、Image、ImageView枢贿、Text殉农、TableView、CollectionView 等局荚。利用這些控件超凳,開發(fā)者可以盡量避免直接使用 UIKit 相關(guān)控件,以獲得更完整的性能提升耀态。
ASDK 的圖層預(yù)合成
有時一個 layer 會包含很多 sub-layer轮傍,而這些 sub-layer 并不需要響應(yīng)觸摸事件,也不需要進(jìn)行動畫和位置調(diào)整首装。ASDK 為此實現(xiàn)了一個被稱為 pre-composing 的技術(shù)创夜,可以把這些 sub-layer 合成渲染為一張圖片。開發(fā)時仙逻,ASNode 已經(jīng)替代了 UIView 和 CALayer挥下;直接使用各種 Node 控件并設(shè)置為 layer backed 后揍魂,ASNode 甚至可以通過預(yù)合成來避免創(chuàng)建內(nèi)部的 UIView 和 CALayer。
通過這種方式棚瘟,把一個大的層級现斋,通過一個大的繪制方法繪制到一張圖上,性能會獲得很大提升偎蘸。CPU 避免了創(chuàng)建 UIKit 對象的資源消耗庄蹋,GPU 避免了多張 texture 合成和渲染的消耗,更少的 bitmap 也意味著更少的內(nèi)存占用迷雪。
ASDK 異步并發(fā)操作
自 iPhone 4S 起限书,iDevice 已經(jīng)都是雙核 CPU 了,現(xiàn)在的 iPad 甚至已經(jīng)更新到 3 核了章咧。充分利用多核的優(yōu)勢倦西、并發(fā)執(zhí)行任務(wù)對保持界面流暢有很大作用。ASDK 把布局計算赁严、文本排版扰柠、圖片/文本/圖形渲染等操作都封裝成較小的任務(wù),并利用 GCD 異步并發(fā)執(zhí)行疼约。如果開發(fā)者使用了 ASNode 相關(guān)的控件卤档,那么這些并發(fā)操作會自動在后臺進(jìn)行,無需進(jìn)行過多配置程剥。
Runloop 任務(wù)分發(fā)
Runloop work distribution 是 ASDK 比較核心的一個技術(shù)劝枣,ASDK 的介紹視頻和文檔中都沒有詳細(xì)展開介紹,所以這里我會多做一些分析织鲸。如果你對 Runloop 還不太了解舔腾,可以看一下我之前的文章 深入理解RunLoop,里面對 ASDK 也有所提及搂擦。
iOS 的顯示系統(tǒng)是由 VSync 信號驅(qū)動的稳诚,VSync 信號由硬件時鐘生成,每秒鐘發(fā)出 60 次(這個值取決設(shè)備硬件盾饮,比如 iPhone 真機(jī)上通常是 59.97)采桃。iOS 圖形服務(wù)接收到 VSync 信號后懒熙,會通過 IPC 通知到 App 內(nèi)丘损。App 的 Runloop 在啟動后會注冊對應(yīng)的 CFRunLoopSource 通過 mach_port 接收傳過來的時鐘信號通知,隨后 Source 的回調(diào)會驅(qū)動整個 App 的動畫與顯示工扎。
Core Animation 在 RunLoop 中注冊了一個 Observer徘钥,監(jiān)聽了 BeforeWaiting 和 Exit 事件。這個 Observer 的優(yōu)先級是 2000000肢娘,低于常見的其他 Observer呈础。當(dāng)一個觸摸事件到來時舆驶,RunLoop 被喚醒,App 中的代碼會執(zhí)行一些操作而钞,比如創(chuàng)建和調(diào)整視圖層級沙廉、設(shè)置 UIView 的 frame、修改 CALayer 的透明度臼节、為視圖添加一個動畫撬陵;這些操作最終都會被 CALayer 捕獲,并通過 CATransaction 提交到一個中間狀態(tài)去(CATransaction 的文檔略有提到這些內(nèi)容网缝,但并不完整)巨税。當(dāng)上面所有操作結(jié)束后,RunLoop 即將進(jìn)入休眠(或者退出)時粉臊,關(guān)注該事件的 Observer 都會得到通知草添。這時 CA 注冊的那個 Observer 就會在回調(diào)中,把所有的中間狀態(tài)合并提交到 GPU 去顯示扼仲;如果此處有動畫远寸,CA 會通過 DisplayLink 等機(jī)制多次觸發(fā)相關(guān)流程。
ASDK 在此處模擬了 Core Animation 的這個機(jī)制:所有針對 ASNode 的修改和提交犀盟,總有些任務(wù)是必需放入主線程執(zhí)行的而晒。當(dāng)出現(xiàn)這種任務(wù)時,ASNode 會把任務(wù)用 ASAsyncTransaction(Group) 封裝并提交到一個全局的容器去阅畴。ASDK 也在 RunLoop 中注冊了一個 Observer倡怎,監(jiān)視的事件和 CA 一樣,但優(yōu)先級比 CA 要低贱枣。當(dāng) RunLoop 進(jìn)入休眠前监署、CA 處理完事件后皂贩,ASDK 就會執(zhí)行該 loop 內(nèi)提交的所有任務(wù)钥勋。具體代碼見這個文件:ASAsyncTransactionGroup裳食。
通過這種機(jī)制砚殿,ASDK 可以在合適的機(jī)會把異步衡瓶、并發(fā)的操作同步到主線程去凳忙,并且能獲得不錯的性能交排。
其他
ASDK 中還有封裝很多高級的功能乙帮,比如滑動列表的預(yù)加載只壳、V2.0添加的新的布局模式等俏拱。ASDK 是一個很龐大的庫,它本身并不推薦你把整個 App 全部都改為 ASDK 驅(qū)動吼句,把最需要提升交互性能的地方用 ASDK 進(jìn)行優(yōu)化就足夠了锅必。
微博 Demo 性能優(yōu)化技巧
我為了演示 YYKit 的功能,實現(xiàn)了微博和 Twitter 的 Demo惕艳,并為它們做了不少性能優(yōu)化搞隐,下面就是優(yōu)化時用到的一些技巧驹愚。
預(yù)排版
當(dāng)獲取到 API JSON 數(shù)據(jù)后,我會把每條 Cell 需要的數(shù)據(jù)都在后臺線程計算并封裝為一個布局對象 CellLayout劣纲。CellLayout 包含所有文本的 CoreText 排版結(jié)果逢捺、Cell 內(nèi)部每個控件的高度、Cell 的整體高度癞季。每個 CellLayout 的內(nèi)存占用并不多蒸甜,所以當(dāng)生成后,可以全部緩存到內(nèi)存余佛,以供稍后使用柠新。這樣,TableView 在請求各個高度函數(shù)時辉巡,不會消耗任何多余計算量恨憎;當(dāng)把 CellLayout 設(shè)置到 Cell 內(nèi)部時,Cell 內(nèi)部也不用再計算布局了郊楣。
對于通常的 TableView 來說憔恳,提前在后臺計算好布局結(jié)果是非常重要的一個性能優(yōu)化點。為了達(dá)到最高性能净蚤,你可能需要犧牲一些開發(fā)速度钥组,不要用 Autolayout 等技術(shù),少用 UILabel 等文本控件今瀑。但如果你對性能的要求并不那么高程梦,可以嘗試用 TableView 的預(yù)估高度的功能,并把每個 Cell 高度緩存下來橘荠。這里有個來自百度知道團(tuán)隊的開源項目可以很方便的幫你實現(xiàn)這一點:FDTemplateLayoutCell屿附。
預(yù)渲染
微博的頭像在某次改版中換成了圓形,所以我也跟進(jìn)了一下哥童。當(dāng)頭像下載下來后挺份,我會在后臺線程將頭像預(yù)先渲染為圓形并單獨保存到一個 ImageCache 中去。
對于 TableView 來說贮懈,Cell 內(nèi)容的離屏渲染會帶來較大的 GPU 消耗匀泊。在 Twitter Demo 中,我為了圖省事兒用到了不少 layer 的圓角屬性朵你,你可以在低性能的設(shè)備(比如 iPad 3)上快速滑動一下這個列表各聘,能感受到雖然列表并沒有較大的卡頓,但是整體的平均幀數(shù)降了下來撬呢。用 Instument 查看時能夠看到 GPU 已經(jīng)滿負(fù)荷運轉(zhuǎn)伦吠,而 CPU 卻比較清閑妆兑。為了避免離屏渲染魂拦,你應(yīng)當(dāng)盡量避免使用 layer 的 border毛仪、corner、shadow芯勘、mask 等技術(shù)箱靴,而盡量在后臺線程預(yù)先繪制好對應(yīng)內(nèi)容。
異步繪制
我只在顯示文本的控件上用到了異步繪制的功能荷愕,但效果很不錯衡怀。我參考 ASDK 的原理,實現(xiàn)了一個簡單的異步繪制控件安疗。這塊代碼我單獨提取出來抛杨,放到了這里:YYAsyncLayer。YYAsyncLayer 是 CALayer 的子類荐类,當(dāng)它需要顯示內(nèi)容(比如調(diào)用了 [layer setNeedDisplay])時怖现,它會向 delegate,也就是 UIView 請求一個異步繪制的任務(wù)玉罐。在異步繪制時屈嗤,Layer 會傳遞一個 BOOL(^isCancelled)() 這樣的 block,繪制代碼可以隨時調(diào)用該 block 判斷繪制任務(wù)是否已經(jīng)被取消吊输。
當(dāng) TableView 快速滑動時饶号,會有大量異步繪制任務(wù)提交到后臺線程去執(zhí)行。但是有時滑動速度過快時季蚂,繪制任務(wù)還沒有完成就可能已經(jīng)被取消了茫船。如果這時仍然繼續(xù)繪制,就會造成大量的 CPU 資源浪費扭屁,甚至阻塞線程并造成后續(xù)的繪制任務(wù)遲遲無法完成透硝。我的做法是盡量快速、提前判斷當(dāng)前繪制任務(wù)是否已經(jīng)被取消疯搅;在繪制每一行文本前濒生,我都會調(diào)用 isCancelled() 來進(jìn)行判斷,保證被取消的任務(wù)能及時退出幔欧,不至于影響后續(xù)操作罪治。
目前有些第三方微博客戶端(比如 VVebo、墨客等)礁蔗,使用了一種方式來避免高速滑動時 Cell 的繪制過程觉义,相關(guān)實現(xiàn)見這個項目:VVeboTableViewDemo。它的原理是浴井,當(dāng)滑動時晒骇,松開手指后,立刻計算出滑動停止時 Cell 的位置,并預(yù)先繪制那個位置附近的幾個 Cell洪囤,而忽略當(dāng)前滑動中的 Cell徒坡。這個方法比較有技巧性,并且對于滑動性能來說提升也很大瘤缩,唯一的缺點就是快速滑動中會出現(xiàn)大量空白內(nèi)容喇完。如果你不想實現(xiàn)比較麻煩的異步繪制但又想保證滑動的流暢性,這個技巧是個不錯的選擇剥啤。
全局并發(fā)控制
當(dāng)我用 concurrent queue 來執(zhí)行大量繪制任務(wù)時锦溪,偶爾會遇到這種問題:
大量的任務(wù)提交到后臺隊列時,某些任務(wù)會因為某些原因(此處是 CGFont 鎖)被鎖住導(dǎo)致線程休眠府怯,或者被阻塞刻诊,concurrent queue 隨后會創(chuàng)建新的線程來執(zhí)行其他任務(wù)。當(dāng)這種情況變多時牺丙,或者 App 中使用了大量 concurrent queue 來執(zhí)行較多任務(wù)時坏逢,App 在同一時刻就會存在幾十個線程同時運行、創(chuàng)建赘被、銷毀是整。CPU 是用時間片輪轉(zhuǎn)來實現(xiàn)線程并發(fā)的,盡管 concurrent queue 能控制線程的優(yōu)先級民假,但當(dāng)大量線程同時創(chuàng)建運行銷毀時浮入,這些操作仍然會擠占掉主線程的 CPU 資源。ASDK 有個 Feed 列表的 Demo:SocialAppLayout羊异,當(dāng)列表內(nèi) Cell 過多事秀,并且非常快速的滑動時野舶,界面仍然會出現(xiàn)少量卡頓易迹,我謹(jǐn)慎的猜測可能與這個問題有關(guān)。
使用 concurrent queue 時不可避免會遇到這種問題平道,但使用 serial queue 又不能充分利用多核 CPU 的資源睹欲。我寫了一個簡單的工具 YYDispatchQueuePool,為不同優(yōu)先級創(chuàng)建和 CPU 數(shù)量相同的 serial queue一屋,每次從 pool 中獲取 queue 時窘疮,會輪詢返回其中一個 queue。我把 App 內(nèi)所有異步操作冀墨,包括圖像解碼闸衫、對象釋放、異步繪制等诽嘉,都按優(yōu)先級不同放入了全局的 serial queue 中執(zhí)行蔚出,這樣盡量避免了過多線程導(dǎo)致的性能問題弟翘。
更高效的異步圖片加載
SDWebImage 在這個 Demo 里仍然會產(chǎn)生少量性能問題,并且有些地方不能滿足我的需求骄酗,所以我自己實現(xiàn)了一個性能更高的圖片加載庫稀余。在顯示簡單的單張圖片時,利用 UIView.layer.contents 就足夠了酥筝,沒必要使用 UIImageView 帶來額外的資源消耗,為此我在 CALayer 上添加了 setImageWithURL 等方法雏门。除此之外嘿歌,我還把圖片解碼等操作通過 YYDispatchQueuePool 進(jìn)行管理,控制了 App 總線程數(shù)量茁影。
其他可以改進(jìn)的地方
上面這些優(yōu)化做完后宙帝,微博 Demo 已經(jīng)非常流暢了,但在我的設(shè)想中募闲,仍然有一些進(jìn)一步優(yōu)化的技巧步脓,但限于時間和精力我并沒有實現(xiàn),下面簡單列一下:
列表中有不少視覺元素并不需要觸摸事件浩螺,這些元素可以用 ASDK 的圖層合成技術(shù)預(yù)先繪制為一張圖靴患。
再進(jìn)一步減少每個 Cell 內(nèi)圖層的數(shù)量,用 CALayer 替換掉 UIView要出。
目前每個 Cell 的類型都是相同的鸳君,但顯示的內(nèi)容卻各部一樣,比如有的 Cell 有圖片患蹂,有的 Cell 里是卡片或颊。把 Cell 按類型劃分,進(jìn)一步減少 Cell 內(nèi)不必要的視圖對象和操作传于,應(yīng)該能有一些效果囱挑。
把需要放到主線程執(zhí)行的任務(wù)劃分為足夠小的塊,并通過 Runloop 來進(jìn)行調(diào)度沼溜,在每個 Loop 里判斷下一次 VSync 的時間平挑,并在下次 VSync 到來前,把當(dāng)前未執(zhí)行完的任務(wù)延遲到下一個機(jī)會去系草。這個只是我的一個設(shè)想弹惦,并不一定能實現(xiàn)或起作用。
如何評測界面的流暢度
最后還是要提一下悄但,“過早的優(yōu)化是萬惡之源”棠隐,在需求未定,性能問題不明顯時檐嚣,沒必要嘗試做優(yōu)化助泽,而要盡量正確的實現(xiàn)功能啰扛。做性能優(yōu)化時,也最好是走修改代碼 -> Profile -> 修改代碼這樣一個流程嗡贺,優(yōu)先解決最值得優(yōu)化的地方隐解。
如果你需要一個明確的 FPS 指示器,可以嘗試一下 KMCGeigerCounter诫睬。對于 CPU 的卡頓煞茫,它可以通過內(nèi)置的 CADisplayLink 檢測出來;對于 GPU 帶來的卡頓摄凡,它用了一個 1×1 的 SKView 來進(jìn)行監(jiān)視续徽。這個項目有兩個小問題:SKView 雖然能監(jiān)視到 GPU 的卡頓,但引入 SKView 本身就會對 CPU/GPU 帶來額外的一點的資源消耗亲澡;這個項目在 iOS 9 下有一些兼容問題钦扭,需要稍作調(diào)整。
我自己也寫了個簡單的 FPS 指示器:FPSLabel 只有幾十行代碼床绪,僅用到了 CADisplayLink 來監(jiān)視 CPU 的卡頓問題客情。雖然不如上面這個工具完善,但日常使用沒有太大問題癞己。
最后膀斋,用 Instuments 的 GPU Driver 預(yù)設(shè),能夠?qū)崟r查看到 CPU 和 GPU 的資源消耗痹雅。在這個預(yù)設(shè)內(nèi)概页,你能查看到幾乎所有與顯示有關(guān)的數(shù)據(jù),比如 Texture 數(shù)量练慕、CA 提交的頻率惰匙、GPU 消耗等,在定位界面卡頓的問題時铃将,這是最好的工具项鬼。