演示項(xiàng)目
在開始技術(shù)討論前,你可以先下載我寫的 Demo 跑到真機(jī)上體驗(yàn)一下:https://github.com/ibireme/YYKit业汰。 Demo 里包含一個(gè)微博的 Feed 列表较曼、發(fā)布視圖,還包含一個(gè) Twitter 的 Feed 列表逛绵。為了公平起見垮媒,所有界面和交互我都從官方應(yīng)用原封不動(dòng)的抄了過來,數(shù)據(jù)也都是從官方應(yīng)用抓取的贝奇。你也可以自己抓取數(shù)據(jù)替換掉 Demo 中的數(shù)據(jù)虹菲,方便進(jìn)行對(duì)比。盡管官方應(yīng)用背后的功能更多更為復(fù)雜掉瞳,但不至于會(huì)帶來太大的交互性能差異毕源。
這個(gè) Demo 最低可以運(yùn)行在 iOS 6 上,所以你可以把它跑到老設(shè)備上體驗(yàn)一下菠赚。在我的測(cè)試中脑豹,即使在 iPhone 4S 或者 iPad 3 上郑藏,Demo 列表在快速滑動(dòng)時(shí)仍然能保持 50~60 FPS 的流暢交互衡查,而其他諸如微博、朋友圈等應(yīng)用的列表視圖在滑動(dòng)時(shí)已經(jīng)有很嚴(yán)重的卡頓了必盖。
微博的 Demo 有大約四千行代碼拌牲,Twitter 的只有兩千行左右代碼,第三方庫只用到了 YYKit歌粥,文件數(shù)量比較少塌忽,方便查看。好了失驶,下面是正文土居。
屏幕顯示圖像的原理
首先從過去的 CRT 顯示器原理說起。CRT 的電子槍按照上面方式嬉探,從上到下一行行掃描擦耀,掃描完成后顯示器就呈現(xiàn)一幀畫面,隨后電子槍回到初始位置繼續(xù)下一次掃描涩堤。為了把顯示器的顯示過程和系統(tǒng)的視頻控制器進(jìn)行同步队询,顯示器(或者其他硬件)會(huì)用硬件時(shí)鐘產(chǎn)生一系列的定時(shí)信號(hào)砾省。當(dāng)電子槍換到新的一行,準(zhǔn)備進(jìn)行掃描時(shí)晓勇,顯示器會(huì)發(fā)出一個(gè)水平同步信號(hào)(horizonal synchronization),簡(jiǎn)稱 HSync嫉嘀;而當(dāng)一幀畫面繪制完成后,電子槍回復(fù)到原位,準(zhǔn)備畫下一幀前上岗,顯示器會(huì)發(fā)出一個(gè)垂直同步信號(hào)(vertical synchronization),簡(jiǎn)稱 VSync蕴坪。顯示器通常以固定頻率進(jìn)行刷新液茎,這個(gè)刷新率就是 VSync 信號(hào)產(chǎn)生的頻率。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏了辞嗡,但原理仍然沒有變捆等。
通常來說,計(jì)算機(jī)系統(tǒng)中 CPU续室、GPU栋烤、顯示器是以上面這種方式協(xié)同工作的。CPU 計(jì)算好顯示內(nèi)容提交到 GPU挺狰,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū)明郭,隨后視頻控制器會(huì)按照 VSync 信號(hào)逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示丰泊。
在最簡(jiǎn)單的情況下薯定,幀緩沖區(qū)只有一個(gè),這時(shí)幀緩沖區(qū)的讀取和刷新都都會(huì)有比較大的效率問題瞳购。為了解決效率問題话侄,顯示系統(tǒng)通常會(huì)引入兩個(gè)緩沖區(qū),即雙緩沖機(jī)制学赛。在這種情況下年堆,GPU 會(huì)預(yù)先渲染好一幀放入一個(gè)緩沖區(qū)內(nèi),讓視頻控制器讀取盏浇,當(dāng)下一幀渲染好后变丧,GPU 會(huì)直接把視頻控制器的指針指向第二個(gè)緩沖器。如此一來效率會(huì)有很大的提升绢掰。
雙緩沖雖然能解決效率問題痒蓬,但會(huì)引入一個(gè)新的問題。當(dāng)視頻控制器還未讀取完成時(shí)滴劲,即屏幕內(nèi)容剛顯示一半時(shí)攻晒,GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個(gè)緩沖區(qū)進(jìn)行交換后,視頻控制器就會(huì)把新的一幀數(shù)據(jù)的下半段顯示到屏幕上哑芹,造成畫面撕裂現(xiàn)象炎辨,如下圖:
為了解決這個(gè)問題,GPU 通常有一個(gè)機(jī)制叫做垂直同步(簡(jiǎn)寫也是 V-Sync)聪姿,當(dāng)開啟垂直同步后碴萧,GPU 會(huì)等待顯示器的 VSync 信號(hào)發(fā)出后乙嘀,才進(jìn)行新的一幀渲染和緩沖區(qū)更新。這樣能解決畫面撕裂現(xiàn)象破喻,也增加了畫面流暢度虎谢,但需要消費(fèi)更多的計(jì)算資源,也會(huì)帶來部分延遲曹质。
那么目前主流的移動(dòng)設(shè)備是什么情況呢婴噩?從網(wǎng)上查到的資料可以知道,iOS 設(shè)備會(huì)始終使用雙緩存羽德,并開啟垂直同步几莽。而安卓設(shè)備直到 4.1 版本,Google 才開始引入這種機(jī)制宅静,目前安卓系統(tǒng)是三緩存+垂直同步章蚣。
卡頓產(chǎn)生的原因和解決方案
在 VSync 信號(hào)到來后,系統(tǒng)圖形服務(wù)會(huì)通過 CADisplayLink 等機(jī)制通知 App姨夹,App 主線程開始在 CPU 中計(jì)算顯示內(nèi)容纤垂,比如視圖的創(chuàng)建、布局計(jì)算磷账、圖片解碼峭沦、文本繪制等。隨后 CPU 會(huì)將計(jì)算好的內(nèi)容提交到 GPU 去逃糟,由 GPU 進(jìn)行變換吼鱼、合成、渲染履磨。隨后 GPU 會(huì)把渲染結(jié)果提交到幀緩沖區(qū)去蛉抓,等待下一次 VSync 信號(hào)到來時(shí)顯示到屏幕上庆尘。由于垂直同步的機(jī)制剃诅,如果在一個(gè) VSync 時(shí)間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交驶忌,則那一幀就會(huì)被丟棄矛辕,等待下一次機(jī)會(huì)再顯示,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變付魔。這就是界面卡頓的原因聊品。
從上面的圖中可以看到,CPU 和 GPU 不論哪個(gè)阻礙了顯示流程几苍,都會(huì)造成掉幀現(xiàn)象翻屈。所以開發(fā)時(shí),也需要分別對(duì) CPU 和 GPU 壓力進(jìn)行評(píng)估和優(yōu)化妻坝。
CPU 資源消耗原因和解決方案
對(duì)象創(chuàng)建
對(duì)象的創(chuàng)建會(huì)分配內(nèi)存伸眶、調(diào)整屬性惊窖、甚至還有讀取文件等操作,比較消耗 CPU 資源厘贼。盡量用輕量的對(duì)象代替重量的對(duì)象界酒,可以對(duì)性能有所優(yōu)化。比如 CALayer 比 UIView 要輕量許多嘴秸,那么不需要響應(yīng)觸摸事件的控件毁欣,用 CALayer 顯示會(huì)更加合適。如果對(duì)象不涉及 UI 操作岳掐,則盡量放到后臺(tái)線程去創(chuàng)建凭疮,但可惜的是包含有 CALayer 的控件,都只能在主線程創(chuàng)建和操作串述。通過 Storyboard 創(chuàng)建視圖對(duì)象時(shí)哭尝,其資源消耗會(huì)比直接通過代碼創(chuàng)建對(duì)象要大非常多,在性能敏感的界面里剖煌,Storyboard 并不是一個(gè)好的技術(shù)選擇材鹦。
盡量推遲對(duì)象創(chuàng)建的時(shí)間,并把對(duì)象的創(chuàng)建分散到多個(gè)任務(wù)中去耕姊。盡管這實(shí)現(xiàn)起來比較麻煩桶唐,并且?guī)淼膬?yōu)勢(shì)并不多,但如果有能力做茉兰,還是要盡量嘗試一下尤泽。如果對(duì)象可以復(fù)用,并且復(fù)用的代價(jià)比釋放规脸、創(chuàng)建新對(duì)象要小坯约,那么這類對(duì)象應(yīng)當(dāng)盡量放到一個(gè)緩存池里復(fù)用。
對(duì)象調(diào)整
對(duì)象的調(diào)整也經(jīng)常是消耗 CPU 資源的地方莫鸭。這里特別說一下 CALayer:CALayer 內(nèi)部并沒有屬性闹丐,當(dāng)調(diào)用屬性方法時(shí),它內(nèi)部是通過運(yùn)行時(shí) resolveInstanceMethod 為對(duì)象臨時(shí)添加一個(gè)方法被因,并把對(duì)應(yīng)屬性值保存到內(nèi)部的一個(gè) Dictionary 里卿拴,同時(shí)還會(huì)通知 delegate、創(chuàng)建動(dòng)畫等等梨与,非常消耗資源堕花。UIView 的關(guān)于顯示相關(guān)的屬性(比如 frame/bounds/transform)等實(shí)際上都是 CALayer 屬性映射來的,所以對(duì) UIView 的這些屬性進(jìn)行調(diào)整時(shí)粥鞋,消耗的資源要遠(yuǎn)大于一般的屬性缘挽。對(duì)此你在應(yīng)用中,應(yīng)該盡量減少不必要的屬性修改。
當(dāng)視圖層次調(diào)整時(shí)壕曼,UIView杠袱、CALayer 之間會(huì)出現(xiàn)很多方法調(diào)用與通知,所以在優(yōu)化性能時(shí)窝稿,應(yīng)該盡量避免調(diào)整視圖層次楣富、添加和移除視圖。
對(duì)象銷毀
對(duì)象的銷毀雖然消耗資源不多伴榔,但累積起來也是不容忽視的纹蝴。通常當(dāng)容器類持有大量對(duì)象時(shí),其銷毀時(shí)的資源消耗就非常明顯踪少。同樣的塘安,如果對(duì)象可以放到后臺(tái)線程去釋放,那就挪到后臺(tái)線程去援奢。這里有個(gè)小 Tip:把對(duì)象捕獲到 block 中兼犯,然后扔到后臺(tái)隊(duì)列去隨便發(fā)送個(gè)消息以避免編譯器警告,就可以讓對(duì)象在后臺(tái)線程銷毀了集漾。
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
布局計(jì)算
視圖布局的計(jì)算是 App 中最為常見的消耗 CPU 資源的地方切黔。如果能在后臺(tái)線程提前計(jì)算好視圖布局、并且對(duì)視圖布局進(jìn)行緩存具篇,那么這個(gè)地方基本就不會(huì)產(chǎn)生性能問題了纬霞。
不論通過何種技術(shù)對(duì)視圖進(jìn)行布局,其最終都會(huì)落到對(duì) UIView.frame/bounds/center 等屬性的調(diào)整上驱显。上面也說過诗芜,對(duì)這些屬性的調(diào)整非常消耗資源,所以盡量提前計(jì)算好布局埃疫,在需要時(shí)一次性調(diào)整好對(duì)應(yīng)屬性伏恐,而不要多次、頻繁的計(jì)算和調(diào)整這些屬性栓霜。
Autolayout
Autolayout 是蘋果本身提倡的技術(shù)翠桦,在大部分情況下也能很好的提升開發(fā)效率,但是 Autolayout 對(duì)于復(fù)雜視圖來說常常會(huì)產(chǎn)生嚴(yán)重的性能問題叙淌。隨著視圖數(shù)量的增長(zhǎng)秤掌,Autolayout 帶來的 CPU 消耗會(huì)呈指數(shù)級(jí)上升。具體數(shù)據(jù)可以看這個(gè)文章:http://pilky.me/36/鹰霍。 如果你不想手動(dòng)調(diào)整 frame 等屬性,你可以用一些工具方法替代(比如常見的 left/right/top/bottom/width/height 快捷屬性)茵乱,或者使用 ComponentKit茂洒、AsyncDisplayKit 等框架。
文本計(jì)算
如果一個(gè)界面中包含大量文本(比如微博微信朋友圈等)瓶竭,文本的寬高計(jì)算會(huì)占用很大一部分資源督勺,并且不可避免渠羞。如果你對(duì)文本顯示沒有特殊要求,可以參考下 UILabel 內(nèi)部的實(shí)現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來計(jì)算文本寬高智哀,用 -[NSAttributedString drawWithRect:options:context:] 來繪制文本次询。盡管這兩個(gè)方法性能不錯(cuò),但仍舊需要放到后臺(tái)線程進(jìn)行以避免阻塞主線程瓷叫。
如果你用 CoreText 繪制文本屯吊,那就可以先生成 CoreText 排版對(duì)象,然后自己計(jì)算了摹菠,并且 CoreText 對(duì)象還能保留以供稍后繪制使用盒卸。
文本渲染
屏幕上能看到的所有文本內(nèi)容控件,包括 UIWebView次氨,在底層都是通過 CoreText 排版蔽介、繪制為 Bitmap 顯示的。常見的文本控件 (UILabel煮寡、UITextView 等)虹蓄,其排版和繪制都是在主線程進(jìn)行的,當(dāng)顯示大量文本時(shí)幸撕,CPU 的壓力會(huì)非常大武花。對(duì)此解決方案只有一個(gè),那就是自定義文本控件杈帐,用 TextKit 或最底層的 CoreText 對(duì)文本異步繪制体箕。盡管這實(shí)現(xiàn)起來非常麻煩,但其帶來的優(yōu)勢(shì)也非常大挑童,CoreText 對(duì)象創(chuàng)建好后累铅,能直接獲取文本的寬高等信息,避免了多次計(jì)算(調(diào)整 UILabel 大小時(shí)算一遍站叼、UILabel 繪制時(shí)內(nèi)部再算一遍)娃兽;CoreText 對(duì)象占用內(nèi)存較少,可以緩存下來以備稍后多次渲染尽楔。
圖片的解碼
當(dāng)你用 UIImage 或 CGImageSource 的那幾個(gè)方法創(chuàng)建圖片時(shí)投储,圖片數(shù)據(jù)并不會(huì)立刻解碼。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去阔馋,并且 CALayer 被提交到 GPU 前玛荞,CGImage 中的數(shù)據(jù)才會(huì)得到解碼。這一步是發(fā)生在主線程的呕寝,并且不可避免勋眯。如果想要繞開這個(gè)機(jī)制,常見的做法是在后臺(tái)線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創(chuàng)建圖片客蹋。目前常見的網(wǎng)絡(luò)圖片庫都自帶這個(gè)功能塞蹭。
圖像的繪制
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中,然后從畫布創(chuàng)建圖片并顯示這樣一個(gè)過程讶坯。這個(gè)最常見的地方就是 [UIView drawRect:] 里面了番电。由于 CoreGraphic 方法通常都是線程安全的,所以圖像的繪制可以很容易的放到后臺(tái)線程進(jìn)行辆琅。一個(gè)簡(jiǎn)單異步繪制的過程大致如下(實(shí)際情況會(huì)比這個(gè)復(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 資源消耗原因和解決方案
相對(duì)于 CPU 來說,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點(diǎn)描述(三角形)涎跨,應(yīng)用變換(transform)洼冻、混合并渲染,然后輸出到屏幕上隅很。通常你所能看到的內(nèi)容撞牢,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。
紋理的渲染
所有的 Bitmap叔营,包括圖片屋彪、文本、柵格化的內(nèi)容绒尊,最終都要由內(nèi)存提交到顯存畜挥,綁定為 GPU Texture。不論是提交到顯存的過程婴谱,還是 GPU 調(diào)整和渲染 Texture 的過程蟹但,都要消耗不少 GPU 資源。當(dāng)在較短時(shí)間顯示大量圖片時(shí)(比如 TableView 存在非常多的圖片并且快速滑動(dòng)時(shí))谭羔,CPU 占用率很低华糖,GPU 占用非常高,界面仍然會(huì)掉幀瘟裸。避免這種情況的方法只能是盡量減少在短時(shí)間內(nèi)大量圖片的顯示客叉,盡可能將多張圖片合成為一張進(jìn)行顯示。
當(dāng)圖片過大话告,超過 GPU 的最大紋理尺寸時(shí)兼搏,圖片需要先由 CPU 進(jìn)行預(yù)處理,這對(duì) CPU 和 GPU 都會(huì)帶來額外的資源消耗沙郭。目前來說佛呻,iPhone 4S 以上機(jī)型,紋理尺寸上限都是 4096×4096棠绘,更詳細(xì)的資料可以看這里:iosres.com件相。所以再扭,盡量不要讓圖片和視圖的大小超過這個(gè)值氧苍。
視圖的混合 (Composing)
當(dāng)多個(gè)視圖(或者說 CALayer)重疊在一起顯示時(shí)夜矗,GPU 會(huì)首先把他們混合到一起。如果視圖結(jié)構(gòu)過于復(fù)雜让虐,混合的過程也會(huì)消耗很多 GPU 資源紊撕。為了減輕這種情況的 GPU 消耗,應(yīng)用應(yīng)當(dāng)盡量減少視圖數(shù)量和層次赡突,并在不透明的視圖里標(biāo)明 opaque 屬性以避免無用的 Alpha 通道合成对扶。當(dāng)然,這也可以用上面的方法惭缰,把多個(gè)視圖預(yù)先渲染為一張圖片來顯示浪南。
圖形的生成。
CALayer 的 border漱受、圓角络凿、陰影、遮罩(mask)昂羡,CASharpLayer 的矢量圖形顯示絮记,通常會(huì)觸發(fā)離屏渲染(offscreen rendering),而離屏渲染通常發(fā)生在 GPU 中虐先。當(dāng)一個(gè)列表視圖中出現(xiàn)大量圓角的 CALayer怨愤,并且快速滑動(dòng)時(shí),可以觀察到 GPU 資源已經(jīng)占滿蛹批,而 CPU 資源消耗很少撰洗。這時(shí)界面仍然能正常滑動(dòng)腐芍,但平均幀數(shù)會(huì)降到很低差导。為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性甸赃,但這會(huì)把原本離屏渲染的操作轉(zhuǎn)嫁到 CPU 上去柿汛。對(duì)于只需要圓角的某些場(chǎng)合,也可以用一張已經(jīng)繪制好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果埠对。最徹底的解決辦法络断,就是把需要顯示的圖形在后臺(tái)線程繪制為圖片,避免使用圓角项玛、陰影貌笨、遮罩等屬性。
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 開源的一個(gè)用于保持 iOS 界面流暢的庫襟沮,我從中學(xué)到了很多東西锥惋,所以下面我會(huì)花較大的篇幅來對(duì)其進(jìn)行介紹和分析昌腰。
ASDK 的由來
ASDK 的作者是 Scott Goodson (Linkedin),
他曾經(jīng)在蘋果工作膀跌,負(fù)責(zé) iOS 的一些內(nèi)置應(yīng)用的開發(fā)遭商,比如股票、計(jì)算器捅伤、地圖劫流、鐘表、設(shè)置丛忆、Safari 等祠汇,當(dāng)然他也參與了 UIKit framework 的開發(fā)。后來他加入 Facebook 后熄诡,負(fù)責(zé) Paper 的開發(fā)可很,創(chuàng)建并開源了 AsyncDisplayKit。目前他在 Pinterest 和 Instagram 負(fù)責(zé) iOS 開發(fā)和用戶體驗(yàn)的提升等工作凰浮。
ASDK 自 2014 年 6 月開源我抠,10 月發(fā)布 1.0 版。目前 ASDK 即將要發(fā)布 2.0 版导坟。
V2.0 增加了更多布局相關(guān)的代碼屿良,ComponentKit 團(tuán)隊(duì)為此貢獻(xiàn)很多。
現(xiàn)在 Github 的 master 分支上的版本是 V1.9.1惫周,已經(jīng)包含了 V2.0 的全部?jī)?nèi)容尘惧。
ASDK 的資料
想要了解 ASDK 的原理和細(xì)節(jié),最好從下面幾個(gè)視頻開始:
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
前兩個(gè)視頻內(nèi)容大同小異递递,都是介紹 ASDK 的基本原理喷橙,附帶介紹 POP 等其他項(xiàng)目。
后一個(gè)視頻增加了 ASDK 2.0 的新特性的介紹登舞。
除此之外贰逾,還可以到 Github Issues 里看一下 ASDK 相關(guān)的討論,下面是幾個(gè)比較重要的內(nèi)容:
關(guān)于 Runloop Dispatch
關(guān)于 ComponentKit 和 ASDK 的區(qū)別
為什么不支持 Storyboard 和 Autolayout
如何評(píng)測(cè)界面的流暢度
之后菠秒,還可以到 Google Groups 來查看和討論更多內(nèi)容:
https://groups.google.com/forum/#!forum/asyncdisplaykit
ASDK 的基本原理
ASDK 認(rèn)為疙剑,阻塞主線程的任務(wù),主要分為上面這三大類践叠。文本和布局的計(jì)算言缤、渲染、解碼禁灼、繪制都可以通過各種方式異步執(zhí)行管挟,但 UIKit 和 Core Animation 相關(guān)操作必需在主線程進(jìn)行。ASDK 的目標(biāo)弄捕,就是盡量把這些任務(wù)從主線程挪走僻孝,而挪不走的导帝,就盡量?jī)?yōu)化性能。
為了達(dá)成這一目標(biāo)穿铆,ASDK 嘗試對(duì) UIKit 組件進(jìn)行封裝:
這是常見的 UIView 和 CALayer 的關(guān)系:View 持有 Layer 用于顯示您单,View 中大部分顯示屬性實(shí)際是從 Layer 映射而來;Layer 的 delegate 在這里是 View悴务,當(dāng)其屬性改變睹限、動(dòng)畫產(chǎn)生時(shí)譬猫,View 能夠得到通知讯檐。UIView 和 CALayer 不是線程安全的,并且只能在主線程創(chuàng)建染服、訪問和銷毀别洪。
ASDK 為此創(chuàng)建了 ASDisplayNode 類,包裝了常見的視圖屬性(比如 frame/bounds/alpha/transform/backgroundColor/superNode/subNodes 等)柳刮,然后它用 UIView->CALayer 相同的方式挖垛,實(shí)現(xiàn)了 ASNode->UIView 這樣一個(gè)關(guān)系。
當(dāng)不需要響應(yīng)觸摸事件時(shí)秉颗,ASDisplayNode 可以被設(shè)置為 layer backed痢毒,即 ASDisplayNode 充當(dāng)了原來 UIView 的功能,節(jié)省了更多資源蚕甥。
與 UIView 和 CALayer 不同哪替,ASDisplayNode 是線程安全的,它可以在后臺(tái)線程創(chuàng)建和修改菇怀。Node 剛創(chuàng)建時(shí)凭舶,并不會(huì)在內(nèi)部新建 UIView 和 CALayer,直到第一次在主線程訪問 view 或 layer 屬性時(shí)爱沟,它才會(huì)在內(nèi)部生成對(duì)應(yīng)的對(duì)象帅霜。當(dāng)它的屬性(比如frame/transform)改變后,它并不會(huì)立刻同步到其持有的 view 或 layer 去呼伸,而是把被改變的屬性保存到內(nèi)部的一個(gè)中間變量身冀,稍后在需要時(shí),再通過某個(gè)機(jī)制一次性設(shè)置到內(nèi)部的 view 或 layer括享。
通過模擬和封裝 UIView/CALayer搂根,開發(fā)者可以把代碼中的 UIView 替換為 ASNode,很大的降低了開發(fā)和學(xué)習(xí)成本奶浦,同時(shí)能獲得 ASDK 底層大量的性能優(yōu)化兄墅。為了方便使用, ASDK 把大量常用控件都封裝成了 ASNode 的子類澳叉,比如 Button隙咸、Control沐悦、Cell、Image五督、ImageView藏否、Text、TableView充包、CollectionView 等副签。利用這些控件,開發(fā)者可以盡量避免直接使用 UIKit 相關(guān)控件基矮,以獲得更完整的性能提升淆储。
ASDK 的圖層預(yù)合成
有時(shí)一個(gè) layer 會(huì)包含很多 sub-layer,而這些 sub-layer 并不需要響應(yīng)觸摸事件家浇,也不需要進(jìn)行動(dòng)畫和位置調(diào)整本砰。ASDK 為此實(shí)現(xiàn)了一個(gè)被稱為 pre-composing 的技術(shù),可以把這些 sub-layer 合成渲染為一張圖片钢悲。開發(fā)時(shí)点额,ASNode 已經(jīng)替代了 UIView 和 CALayer;直接使用各種 Node 控件并設(shè)置為 layer backed 后莺琳,ASNode 甚至可以通過預(yù)合成來避免創(chuàng)建內(nèi)部的 UIView 和 CALayer还棱。
通過這種方式,把一個(gè)大的層級(jí)惭等,通過一個(gè)大的繪制方法繪制到一張圖上珍手,性能會(huì)獲得很大提升。CPU 避免了創(chuàng)建 UIKit 對(duì)象的資源消耗咕缎,GPU 避免了多張 texture 合成和渲染的消耗珠十,更少的 bitmap 也意味著更少的內(nèi)存占用。
ASDK 異步并發(fā)操作
自 iPhone 4S 起凭豪,iDevice 已經(jīng)都是雙核 CPU 了焙蹭,現(xiàn)在的 iPad 甚至已經(jīng)更新到 3 核了。充分利用多核的優(yōu)勢(shì)嫂伞、并發(fā)執(zhí)行任務(wù)對(duì)保持界面流暢有很大作用孔厉。ASDK 把布局計(jì)算、文本排版帖努、圖片/文本/圖形渲染等操作都封裝成較小的任務(wù)撰豺,并利用 GCD 異步并發(fā)執(zhí)行。如果開發(fā)者使用了 ASNode 相關(guān)的控件拼余,那么這些并發(fā)操作會(huì)自動(dòng)在后臺(tái)進(jìn)行污桦,無需進(jìn)行過多配置。
Runloop 任務(wù)分發(fā)
Runloop work distribution 是 ASDK 比較核心的一個(gè)技術(shù)匙监,ASDK 的介紹視頻和文檔中都沒有詳細(xì)展開介紹凡橱,所以這里我會(huì)多做一些分析小作。如果你對(duì) Runloop 還不太了解,可以看一下我之前的文章深入理解RunLoop稼钩,里面對(duì) ASDK 也有所提及顾稀。
iOS 的顯示系統(tǒng)是由 VSync 信號(hào)驅(qū)動(dòng)的,VSync 信號(hào)由硬件時(shí)鐘生成坝撑,每秒鐘發(fā)出 60 次(這個(gè)值取決設(shè)備硬件静秆,比如 iPhone 真機(jī)上通常是 59.97)。iOS 圖形服務(wù)接收到 VSync 信號(hào)后巡李,會(huì)通過 IPC 通知到 App 內(nèi)抚笔。App 的 Runloop 在啟動(dòng)后會(huì)注冊(cè)對(duì)應(yīng)的 CFRunLoopSource 通過 mach_port 接收傳過來的時(shí)鐘信號(hào)通知,隨后 Source 的回調(diào)會(huì)驅(qū)動(dòng)整個(gè) App 的動(dòng)畫與顯示击儡。
Core Animation 在 RunLoop 中注冊(cè)了一個(gè) Observer塔沃,監(jiān)聽了 BeforeWaiting 和 Exit 事件。這個(gè) Observer 的優(yōu)先級(jí)是 2000000阳谍,低于常見的其他 Observer。當(dāng)一個(gè)觸摸事件到來時(shí)螃概,RunLoop 被喚醒矫夯,App 中的代碼會(huì)執(zhí)行一些操作,比如創(chuàng)建和調(diào)整視圖層級(jí)吊洼、設(shè)置 UIView 的 frame训貌、修改 CALayer 的透明度、為視圖添加一個(gè)動(dòng)畫冒窍;這些操作最終都會(huì)被 CALayer 捕獲递沪,并通過 CATransaction 提交到一個(gè)中間狀態(tài)去(CATransaction 的文檔略有提到這些內(nèi)容,但并不完整)综液。當(dāng)上面所有操作結(jié)束后款慨,RunLoop 即將進(jìn)入休眠(或者退出)時(shí),關(guān)注該事件的 Observer 都會(huì)得到通知谬莹。這時(shí) CA 注冊(cè)的那個(gè) Observer 就會(huì)在回調(diào)中檩奠,把所有的中間狀態(tài)合并提交到 GPU 去顯示;如果此處有動(dòng)畫附帽,CA 會(huì)通過 DisplayLink 等機(jī)制多次觸發(fā)相關(guān)流程。
ASDK 在此處模擬了 Core Animation 的這個(gè)機(jī)制:所有針對(duì) ASNode 的修改和提交,總有些任務(wù)是必需放入主線程執(zhí)行的抡句。當(dāng)出現(xiàn)這種任務(wù)時(shí)角溃,ASNode 會(huì)把任務(wù)用 ASAsyncTransaction(Group) 封裝并提交到一個(gè)全局的容器去。ASDK 也在 RunLoop 中注冊(cè)了一個(gè) Observer喳钟,監(jiān)視的事件和 CA 一樣屁使,但優(yōu)先級(jí)比 CA 要低欠啤。當(dāng) RunLoop 進(jìn)入休眠前、CA 處理完事件后屋灌,ASDK 就會(huì)執(zhí)行該 loop 內(nèi)提交的所有任務(wù)洁段。具體代碼見這個(gè)文件:ASAsyncTransactionGroup。
通過這種機(jī)制共郭,ASDK 可以在合適的機(jī)會(huì)把異步祠丝、并發(fā)的操作同步到主線程去,并且能獲得不錯(cuò)的性能除嘹。
其他
ASDK 中還有封裝很多高級(jí)的功能写半,比如滑動(dòng)列表的預(yù)加載、V2.0添加的新的布局模式等尉咕。ASDK 是一個(gè)很龐大的庫叠蝇,它本身并不推薦你把整個(gè) App 全部都改為 ASDK 驅(qū)動(dòng),把最需要提升交互性能的地方用 ASDK 進(jìn)行優(yōu)化就足夠了年缎。
微博 Demo 性能優(yōu)化技巧
我為了演示 YYKit 的功能悔捶,實(shí)現(xiàn)了微博和 Twitter 的 Demo,并為它們做了不少性能優(yōu)化单芜,下面就是優(yōu)化時(shí)用到的一些技巧蜕该。
預(yù)排版
當(dāng)獲取到 API JSON 數(shù)據(jù)后,我會(huì)把每條 Cell 需要的數(shù)據(jù)都在后臺(tái)線程計(jì)算并封裝為一個(gè)布局對(duì)象 CellLayout洲鸠。CellLayout 包含所有文本的 CoreText 排版結(jié)果堂淡、Cell 內(nèi)部每個(gè)控件的高度、Cell 的整體高度扒腕。每個(gè) CellLayout 的內(nèi)存占用并不多绢淀,所以當(dāng)生成后,可以全部緩存到內(nèi)存瘾腰,以供稍后使用皆的。這樣,TableView 在請(qǐng)求各個(gè)高度函數(shù)時(shí)居灯,不會(huì)消耗任何多余計(jì)算量祭务;當(dāng)把 CellLayout 設(shè)置到 Cell 內(nèi)部時(shí),Cell 內(nèi)部也不用再計(jì)算布局了怪嫌。
對(duì)于通常的 TableView 來說义锥,提前在后臺(tái)計(jì)算好布局結(jié)果是非常重要的一個(gè)性能優(yōu)化點(diǎn)。為了達(dá)到最高性能岩灭,你可能需要犧牲一些開發(fā)速度拌倍,不要用 Autolayout 等技術(shù),少用 UILabel 等文本控件。但如果你對(duì)性能的要求并不那么高柱恤,可以嘗試用 TableView 的預(yù)估高度的功能数初,并把每個(gè) Cell 高度緩存下來。這里有個(gè)來自百度知道團(tuán)隊(duì)的開源項(xiàng)目可以很方便的幫你實(shí)現(xiàn)這一點(diǎn):FDTemplateLayoutCell梗顺。
預(yù)渲染
微博的頭像在某次改版中換成了圓形泡孩,所以我也跟進(jìn)了一下。當(dāng)頭像下載下來后寺谤,我會(huì)在后臺(tái)線程將頭像預(yù)先渲染為圓形并單獨(dú)保存到一個(gè) ImageCache 中去仑鸥。
對(duì)于 TableView 來說,Cell 內(nèi)容的離屏渲染會(huì)帶來較大的 GPU 消耗变屁。在 Twitter Demo 中眼俊,我為了圖省事兒用到了不少 layer 的圓角屬性,你可以在低性能的設(shè)備(比如 iPad 3)上快速滑動(dòng)一下這個(gè)列表粟关,能感受到雖然列表并沒有較大的卡頓疮胖,但是整體的平均幀數(shù)降了下來。用 Instument 查看時(shí)能夠看到 GPU 已經(jīng)滿負(fù)荷運(yùn)轉(zhuǎn)闷板,而 CPU 卻比較清閑澎灸。為了避免離屏渲染,你應(yīng)當(dāng)盡量避免使用 layer 的 border蛔垢、corner击孩、shadow、mask 等技術(shù)鹏漆,而盡量在后臺(tái)線程預(yù)先繪制好對(duì)應(yīng)內(nèi)容。
異步繪制
我只在顯示文本的控件上用到了異步繪制的功能创泄,但效果很不錯(cuò)艺玲。我參考 ASDK 的原理,實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的異步繪制控件鞠抑。這塊代碼我單獨(dú)提取出來饭聚,放到了這里:YYAsyncLayer。YYAsyncLayer 是 CALayer 的子類搁拙,當(dāng)它需要顯示內(nèi)容(比如調(diào)用了 [layer setNeedDisplay])時(shí)秒梳,它會(huì)向 delegate,也就是 UIView 請(qǐng)求一個(gè)異步繪制的任務(wù)箕速。在異步繪制時(shí)酪碘,Layer 會(huì)傳遞一個(gè) BOOL(^isCancelled)() 這樣的 block,繪制代碼可以隨時(shí)調(diào)用該 block 判斷繪制任務(wù)是否已經(jīng)被取消盐茎。
當(dāng) TableView 快速滑動(dòng)時(shí)兴垦,會(huì)有大量異步繪制任務(wù)提交到后臺(tái)線程去執(zhí)行。但是有時(shí)滑動(dòng)速度過快時(shí),繪制任務(wù)還沒有完成就可能已經(jīng)被取消了探越。如果這時(shí)仍然繼續(xù)繪制狡赐,就會(huì)造成大量的 CPU 資源浪費(fèi),甚至阻塞線程并造成后續(xù)的繪制任務(wù)遲遲無法完成钦幔。我的做法是盡量快速枕屉、提前判斷當(dāng)前繪制任務(wù)是否已經(jīng)被取消;在繪制每一行文本前鲤氢,我都會(huì)調(diào)用 isCancelled() 來進(jìn)行判斷搀擂,保證被取消的任務(wù)能及時(shí)退出,不至于影響后續(xù)操作铜异。
目前有些第三方微博客戶端(比如 VVebo哥倔、墨客等),使用了一種方式來避免高速滑動(dòng)時(shí) Cell 的繪制過程揍庄,相關(guān)實(shí)現(xiàn)見這個(gè)項(xiàng)目:VVeboTableViewDemo咆蒿。它的原理是,當(dāng)滑動(dòng)時(shí)蚂子,松開手指后沃测,立刻計(jì)算出滑動(dòng)停止時(shí) Cell 的位置,并預(yù)先繪制那個(gè)位置附近的幾個(gè) Cell食茎,而忽略當(dāng)前滑動(dòng)中的 Cell蒂破。這個(gè)方法比較有技巧性,并且對(duì)于滑動(dòng)性能來說提升也很大别渔,唯一的缺點(diǎn)就是快速滑動(dòng)中會(huì)出現(xiàn)大量空白內(nèi)容附迷。如果你不想實(shí)現(xiàn)比較麻煩的異步繪制但又想保證滑動(dòng)的流暢性,這個(gè)技巧是個(gè)不錯(cuò)的選擇哎媚。
全局并發(fā)控制
當(dāng)我用 concurrent queue 來執(zhí)行大量繪制任務(wù)時(shí)喇伯,偶爾會(huì)遇到這種問題:
大量的任務(wù)提交到后臺(tái)隊(duì)列時(shí),某些任務(wù)會(huì)因?yàn)槟承┰颍ù颂幨?CGFont 鎖)被鎖住導(dǎo)致線程休眠拨与,或者被阻塞稻据,concurrent queue 隨后會(huì)創(chuàng)建新的線程來執(zhí)行其他任務(wù)。當(dāng)這種情況變多時(shí)买喧,或者 App 中使用了大量 concurrent queue 來執(zhí)行較多任務(wù)時(shí)捻悯,App 在同一時(shí)刻就會(huì)存在幾十個(gè)線程同時(shí)運(yùn)行、創(chuàng)建淤毛、銷毀今缚。CPU 是用時(shí)間片輪轉(zhuǎn)來實(shí)現(xiàn)線程并發(fā)的,盡管 concurrent queue 能控制線程的優(yōu)先級(jí)钱床,但當(dāng)大量線程同時(shí)創(chuàng)建運(yùn)行銷毀時(shí)荚斯,這些操作仍然會(huì)擠占掉主線程的 CPU 資源。ASDK 有個(gè) Feed 列表的 Demo:SocialAppLayout,當(dāng)列表內(nèi) Cell 過多事期,并且非忱暮荆快速的滑動(dòng)時(shí),界面仍然會(huì)出現(xiàn)少量卡頓兽泣,我謹(jǐn)慎的猜測(cè)可能與這個(gè)問題有關(guān)绎橘。
使用 concurrent queue 時(shí)不可避免會(huì)遇到這種問題,但使用 serial queue 又不能充分利用多核 CPU 的資源唠倦。我寫了一個(gè)簡(jiǎn)單的工具 YYDispatchQueuePool称鳞,為不同優(yōu)先級(jí)創(chuàng)建和 CPU 數(shù)量相同的 serial queue,每次從 pool 中獲取 queue 時(shí)稠鼻,會(huì)輪詢返回其中一個(gè) queue冈止。我把 App 內(nèi)所有異步操作,包括圖像解碼候齿、對(duì)象釋放熙暴、異步繪制等,都按優(yōu)先級(jí)不同放入了全局的 serial queue 中執(zhí)行慌盯,這樣盡量避免了過多線程導(dǎo)致的性能問題周霉。
更高效的異步圖片加載
SDWebImage 在這個(gè) Demo 里仍然會(huì)產(chǎn)生少量性能問題,并且有些地方不能滿足我的需求亚皂,所以我自己實(shí)現(xiàn)了一個(gè)性能更高的圖片加載庫俱箱。在顯示簡(jiǎn)單的單張圖片時(shí),利用 UIView.layer.contents 就足夠了灭必,沒必要使用 UIImageView 帶來額外的資源消耗狞谱,為此我在 CALayer 上添加了 setImageWithURL 等方法。除此之外禁漓,我還把圖片解碼等操作通過 YYDispatchQueuePool 進(jìn)行管理芋簿,控制了 App 總線程數(shù)量。
其他可以改進(jìn)的地方
上面這些優(yōu)化做完后璃饱,微博 Demo 已經(jīng)非常流暢了,但在我的設(shè)想中肪康,仍然有一些進(jìn)一步優(yōu)化的技巧荚恶,但限于時(shí)間和精力我并沒有實(shí)現(xiàn),下面簡(jiǎn)單列一下:
列表中有不少視覺元素并不需要觸摸事件磷支,這些元素可以用 ASDK 的圖層合成技術(shù)預(yù)先繪制為一張圖谒撼。
再進(jìn)一步減少每個(gè) Cell 內(nèi)圖層的數(shù)量,用 CALayer 替換掉 UIView雾狈。
目前每個(gè) Cell 的類型都是相同的廓潜,但顯示的內(nèi)容卻各部一樣,比如有的 Cell 有圖片,有的 Cell 里是卡片辩蛋。把 Cell 按類型劃分呻畸,進(jìn)一步減少 Cell 內(nèi)不必要的視圖對(duì)象和操作,應(yīng)該能有一些效果悼院。
把需要放到主線程執(zhí)行的任務(wù)劃分為足夠小的塊伤为,并通過 Runloop 來進(jìn)行調(diào)度,在每個(gè) Loop 里判斷下一次 VSync 的時(shí)間据途,并在下次 VSync 到來前绞愚,把當(dāng)前未執(zhí)行完的任務(wù)延遲到下一個(gè)機(jī)會(huì)去。這個(gè)只是我的一個(gè)設(shè)想颖医,并不一定能實(shí)現(xiàn)或起作用位衩。
如何評(píng)測(cè)界面的流暢度
最后還是要提一下,“過早的優(yōu)化是萬惡之源”熔萧,在需求未定糖驴,性能問題不明顯時(shí),沒必要嘗試做優(yōu)化哪痰,而要盡量正確的實(shí)現(xiàn)功能遂赠。做性能優(yōu)化時(shí),也最好是走修改代碼 -> Profile -> 修改代碼這樣一個(gè)流程晌杰,優(yōu)先解決最值得優(yōu)化的地方跷睦。
如果你需要一個(gè)明確的 FPS 指示器,可以嘗試一下 KMCGeigerCounter肋演。對(duì)于 CPU 的卡頓抑诸,它可以通過內(nèi)置的 CADisplayLink 檢測(cè)出來;對(duì)于 GPU 帶來的卡頓爹殊,它用了一個(gè) 1×1 的 SKView 來進(jìn)行監(jiān)視蜕乡。這個(gè)項(xiàng)目有兩個(gè)小問題:SKView 雖然能監(jiān)視到 GPU 的卡頓,但引入 SKView 本身就會(huì)對(duì) CPU/GPU 帶來額外的一點(diǎn)的資源消耗梗夸;這個(gè)項(xiàng)目在 iOS 9 下有一些兼容問題层玲,需要稍作調(diào)整。
我自己也寫了個(gè)簡(jiǎn)單的 FPS 指示器:FPSLabel 只有幾十行代碼反症,僅用到了 CADisplayLink 來監(jiān)視 CPU 的卡頓問題辛块。雖然不如上面這個(gè)工具完善,但日常使用沒有太大問題铅碍。
最后润绵,用 Instuments 的 GPU Driver 預(yù)設(shè),能夠?qū)崟r(shí)查看到 CPU 和 GPU 的資源消耗胞谈。在這個(gè)預(yù)設(shè)內(nèi)尘盼,你能查看到幾乎所有與顯示有關(guān)的數(shù)據(jù)憨愉,比如 Texture 數(shù)量、CA 提交的頻率卿捎、GPU 消耗等配紫,在定位界面卡頓的問題時(shí),這是最好的工具娇澎。