這篇文章會(huì)非常詳細(xì)的分析 iOS 界面構(gòu)建中的各種性能問(wèn)題以及對(duì)應(yīng)的解決思路集乔,同時(shí)給出一個(gè)開(kāi)源的微博列表實(shí)現(xiàn),通過(guò)實(shí)際的代碼展示如何構(gòu)建流暢的交互条篷。
演示項(xiàng)目
在開(kāi)始技術(shù)討論前肢扯,你可以先下載我寫(xiě)的 Demo 跑到真機(jī)上體驗(yàn)一下:https://github.com/ibireme/YYKit。 Demo 里包含一個(gè)微博的 Feed 列表束倍、發(fā)布視圖,還包含一個(gè) Twitter 的 Feed 列表盟戏。為了公平起見(jiàn)绪妹,所有界面和交互我都從官方應(yīng)用原封不動(dòng)的抄了過(guò)來(lái),數(shù)據(jù)也都是從官方應(yīng)用抓取的柿究。你也可以自己抓取數(shù)據(jù)替換掉 Demo 中的數(shù)據(jù)邮旷,方便進(jìn)行對(duì)比。盡管官方應(yīng)用背后的功能更多更為復(fù)雜笛求,但不至于會(huì)帶來(lái)太大的交互性能差異廊移。
這個(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 的只有兩千行左右代碼,第三方庫(kù)只用到了 YYKit离唐,文件數(shù)量比較少病附,方便查看。好了亥鬓,下面是正文完沪。
首先從過(guò)去的 CRT 顯示器原理說(shuō)起。CRT 的電子槍按照上面方式,從上到下一行行掃描覆积,掃描完成后顯示器就呈現(xiàn)一幀畫(huà)面听皿,隨后電子槍回到初始位置繼續(xù)下一次掃描。為了把顯示器的顯示過(guò)程和系統(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)一幀畫(huà)面繪制完成后椎瘟,電子槍回復(fù)到原位馋没,準(zhǔn)備畫(huà)下一幀前,顯示器會(huì)發(fā)出一個(gè)垂直同步信號(hào)(vertical synchronization)降传,簡(jiǎn)稱 VSync篷朵。顯示器通常以固定頻率進(jìn)行刷新,這個(gè)刷新率就是 VSync 信號(hào)產(chǎn)生的頻率婆排。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏了声旺,但原理仍然沒(méi)有變。
通常來(lái)說(shuō)段只,計(jì)算機(jī)系統(tǒng)中 CPU腮猖、GPU、顯示器是以上面這種方式協(xié)同工作的赞枕。CPU 計(jì)算好顯示內(nèi)容提交到 GPU澈缺,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會(huì)按照 VSync 信號(hào)逐行讀取幀緩沖區(qū)的數(shù)據(jù)炕婶,經(jīng)過(guò)可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示姐赡。
在最簡(jiǎn)單的情況下,幀緩沖區(qū)只有一個(gè)柠掂,這時(shí)幀緩沖區(qū)的讀取和刷新都都會(huì)有比較大的效率問(wèn)題项滑。為了解決效率問(wèn)題,顯示系統(tǒng)通常會(huì)引入兩個(gè)緩沖區(qū)涯贞,即雙緩沖機(jī)制枪狂。在這種情況下,GPU 會(huì)預(yù)先渲染好一幀放入一個(gè)緩沖區(qū)內(nèi)宋渔,讓視頻控制器讀取州疾,當(dāng)下一幀渲染好后,GPU 會(huì)直接把視頻控制器的指針指向第二個(gè)緩沖器皇拣。如此一來(lái)效率會(huì)有很大的提升严蓖。
雙緩沖雖然能解決效率問(wèn)題,但會(huì)引入一個(gè)新的問(wèn)題。當(dāng)視頻控制器還未讀取完成時(shí)谈飒,即屏幕內(nèi)容剛顯示一半時(shí)岂座,GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個(gè)緩沖區(qū)進(jìn)行交換后,視頻控制器就會(huì)把新的一幀數(shù)據(jù)的下半段顯示到屏幕上杭措,造成畫(huà)面撕裂現(xiàn)象费什,如下圖:
為了解決這個(gè)問(wèn)題,GPU 通常有一個(gè)機(jī)制叫做垂直同步(簡(jiǎn)寫(xiě)也是 V-Sync)手素,當(dāng)開(kāi)啟垂直同步后鸳址,GPU 會(huì)等待顯示器的 VSync 信號(hào)發(fā)出后,才進(jìn)行新的一幀渲染和緩沖區(qū)更新泉懦。這樣能解決畫(huà)面撕裂現(xiàn)象稿黍,也增加了畫(huà)面流暢度,但需要消費(fèi)更多的計(jì)算資源崩哩,也會(huì)帶來(lái)部分延遲巡球。
那么目前主流的移動(dòng)設(shè)備是什么情況呢?從網(wǎng)上查到的資料可以知道邓嘹,iOS 設(shè)備會(huì)始終使用雙緩存酣栈,并開(kāi)啟垂直同步。而安卓設(shè)備直到 4.1 版本汹押,Google 才開(kāi)始引入這種機(jī)制矿筝,目前安卓系統(tǒng)是三緩存+垂直同步。
在 VSync 信號(hào)到來(lái)后棚贾,系統(tǒng)圖形服務(wù)會(huì)通過(guò) CADisplayLink 等機(jī)制通知 App窖维,App 主線程開(kāi)始在 CPU 中計(jì)算顯示內(nèi)容,比如視圖的創(chuàng)建妙痹、布局計(jì)算铸史、圖片解碼、文本繪制等细诸。隨后 CPU 會(huì)將計(jì)算好的內(nèi)容提交到 GPU 去沛贪,由 GPU 進(jìn)行變換、合成震贵、渲染。隨后 GPU 會(huì)把渲染結(jié)果提交到幀緩沖區(qū)去水评,等待下一次 VSync 信號(hào)到來(lái)時(shí)顯示到屏幕上猩系。由于垂直同步的機(jī)制,如果在一個(gè) VSync 時(shí)間內(nèi)中燥,CPU 或者 GPU 沒(méi)有完成內(nèi)容提交寇甸,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變拿霉。這就是界面卡頓的原因吟秩。
從上面的圖中可以看到,CPU 和 GPU 不論哪個(gè)阻礙了顯示流程绽淘,都會(huì)造成掉幀現(xiàn)象涵防。所以開(kāi)發(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)建和操作慧域。通過(guò) Storyboard 創(chuàng)建視圖對(duì)象時(shí),其資源消耗會(huì)比直接通過(guò)代碼創(chuàng)建對(duì)象要大非常多浪读,在性能敏感的界面里昔榴,Storyboard 并不是一個(gè)好的技術(shù)選擇。
盡量推遲對(duì)象創(chuàng)建的時(shí)間碘橘,并把對(duì)象的創(chuàng)建分散到多個(gè)任務(wù)中去互订。盡管這實(shí)現(xiàn)起來(lái)比較麻煩,并且?guī)?lái)的優(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 資源的地方温峭。這里特別說(shuō)一下 CALayer:CALayer 內(nèi)部并沒(méi)有屬性,當(dāng)調(diào)用屬性方法時(shí)字支,它內(nèi)部是通過(guò)運(yùn)行時(shí) resolveInstanceMethod 為對(duì)象臨時(shí)添加一個(gè)方法凤藏,并把對(duì)應(yīng)屬性值保存到內(nèi)部的一個(gè) Dictionary 里奸忽,同時(shí)還會(huì)通知 delegate、創(chuàng)建動(dòng)畫(huà)等等揖庄,非常消耗資源栗菜。UIView 的關(guān)于顯示相關(guān)的屬性(比如 frame/bounds/transform)等實(shí)際上都是 CALayer 屬性映射來(lái)的,所以對(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ì)象的銷毀雖然消耗資源不多馍驯,但累積起來(lái)也是不容忽視的。通常當(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 中最為常見(jiàn)的消耗 CPU 資源的地方。如果能在后臺(tái)線程提前計(jì)算好視圖布局蒿涎、并且對(duì)視圖布局進(jìn)行緩存哀托,那么這個(gè)地方基本就不會(huì)產(chǎn)生性能問(wèn)題了。
不論通過(guò)何種技術(shù)對(duì)視圖進(jìn)行布局劳秋,其最終都會(huì)落到對(duì) UIView.frame/bounds/center 等屬性的調(diào)整上仓手。上面也說(shuō)過(guò),對(duì)這些屬性的調(diào)整非常消耗資源玻淑,所以盡量提前計(jì)算好布局嗽冒,在需要時(shí)一次性調(diào)整好對(duì)應(yīng)屬性,而不要多次岁忘、頻繁的計(jì)算和調(diào)整這些屬性辛慰。
Autolayout
Autolayout 是蘋(píng)果本身提倡的技術(shù),在大部分情況下也能很好的提升開(kāi)發(fā)效率干像,但是 Autolayout 對(duì)于復(fù)雜視圖來(lái)說(shuō)常常會(huì)產(chǎn)生嚴(yán)重的性能問(wèn)題帅腌。隨著視圖數(shù)量的增長(zhǎng),Autolayout 帶來(lái)的 CPU 消耗會(huì)呈指數(shù)級(jí)上升麻汰。具體數(shù)據(jù)可以看這個(gè)文章:http://pilky.me/36/速客。 如果你不想手動(dòng)調(diào)整 frame 等屬性,你可以用一些工具方法替代(比如常見(jiàn)的 left/right/top/bottom/width/height 快捷屬性)五鲫,或者使用 ComponentKit溺职、AsyncDisplayKit 等框架。
文本計(jì)算
如果一個(gè)界面中包含大量文本(比如微博微信朋友圈等)位喂,文本的寬高計(jì)算會(huì)占用很大一部分資源浪耘,并且不可避免。如果你對(duì)文本顯示沒(méi)有特殊要求塑崖,可以參考下 UILabel 內(nèi)部的實(shí)現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來(lái)計(jì)算文本寬高七冲,用 -[NSAttributedString drawWithRect:options:context:] 來(lái)繪制文本。盡管這兩個(gè)方法性能不錯(cuò)规婆,但仍舊需要放到后臺(tái)線程進(jìn)行以避免阻塞主線程澜躺。
如果你用 CoreText 繪制文本,那就可以先生成 CoreText 排版對(duì)象抒蚜,然后自己計(jì)算了掘鄙,并且 CoreText 對(duì)象還能保留以供稍后繪制使用。
文本渲染
屏幕上能看到的所有文本內(nèi)容控件嗡髓,包括 UIWebView操漠,在底層都是通過(guò) CoreText 排版、繪制為 Bitmap 顯示的饿这。常見(jiàn)的文本控件 (UILabel浊伙、UITextView 等),其排版和繪制都是在主線程進(jìn)行的蛹稍,當(dāng)顯示大量文本時(shí)吧黄,CPU 的壓力會(huì)非常大。對(duì)此解決方案只有一個(gè)唆姐,那就是自定義文本控件拗慨,用 TextKit 或最底層的 CoreText 對(duì)文本異步繪制。盡管這實(shí)現(xiàn)起來(lái)非常麻煩奉芦,但其帶來(lái)的優(yōu)勢(shì)也非常大赵抢,CoreText 對(duì)象創(chuàng)建好后,能直接獲取文本的寬高等信息声功,避免了多次計(jì)算(調(diào)整 UILabel 大小時(shí)算一遍烦却、UILabel 繪制時(shí)內(nèi)部再算一遍);CoreText 對(duì)象占用內(nèi)存較少先巴,可以緩存下來(lái)以備稍后多次渲染其爵。
圖片的解碼
當(dāng)你用 UIImage 或 CGImageSource 的那幾個(gè)方法創(chuàng)建圖片時(shí)冒冬,圖片數(shù)據(jù)并不會(huì)立刻解碼。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去摩渺,并且 CALayer 被提交到 GPU 前简烤,CGImage 中的數(shù)據(jù)才會(huì)得到解碼。這一步是發(fā)生在主線程的摇幻,并且不可避免横侦。如果想要繞開(kāi)這個(gè)機(jī)制,常見(jiàn)的做法是在后臺(tái)線程先把圖片繪制到 CGBitmapContext 中绰姻,然后從 Bitmap 直接創(chuàng)建圖片枉侧。目前常見(jiàn)的網(wǎng)絡(luò)圖片庫(kù)都自帶這個(gè)功能。
** 圖像的繪制**
圖像的繪制通常是指用那些以 CG 開(kāi)頭的方法把圖像繪制到畫(huà)布中狂芋,然后從畫(huà)布創(chuàng)建圖片并顯示這樣一個(gè)過(guò)程榨馁。這個(gè)最常見(jiàn)的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是線程安全的银酗,所以圖像的繪制可以很容易的放到后臺(tái)線程進(jìn)行辆影。一個(gè)簡(jiǎn)單異步繪制的過(guò)程大致如下(實(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 來(lái)說(shuō)黍特,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點(diǎn)描述(三角形)蛙讥,應(yīng)用變換(transform)、混合并渲染灭衷,然后輸出到屏幕上次慢。通常你所能看到的內(nèi)容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類翔曲。
紋理的渲染
所有的 Bitmap迫像,包括圖片、文本瞳遍、柵格化的內(nèi)容闻妓,最終都要由內(nèi)存提交到顯存,綁定為 GPU Texture掠械。不論是提交到顯存的過(guò)程由缆,還是 GPU 調(diào)整和渲染 Texture 的過(guò)程,都要消耗不少 GPU 資源猾蒂。當(dāng)在較短時(shí)間顯示大量圖片時(shí)(比如 TableView 存在非常多的圖片并且快速滑動(dòng)時(shí))均唉,CPU 占用率很低,GPU 占用非常高肚菠,界面仍然會(huì)掉幀舔箭。避免這種情況的方法只能是盡量減少在短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合成為一張進(jìn)行顯示蚊逢。
當(dāng)圖片過(guò)大层扶,超過(guò) GPU 的最大紋理尺寸時(shí)箫章,圖片需要先由 CPU 進(jìn)行預(yù)處理,這對(duì) CPU 和 GPU 都會(huì)帶來(lái)額外的資源消耗怒医。目前來(lái)說(shuō)炉抒,iPhone 4S 以上機(jī)型奢讨,紋理尺寸上限都是 4096×4096稚叹,更詳細(xì)的資料可以看這里:iosres.com。所以拿诸,盡量不要讓圖片和視圖的大小超過(guò)這個(gè)值扒袖。
視圖的混合 (Composing)
當(dāng)多個(gè)視圖(或者說(shuō) CALayer)重疊在一起顯示時(shí),GPU 會(huì)首先把他們混合到一起亩码。如果視圖結(jié)構(gòu)過(guò)于復(fù)雜季率,混合的過(guò)程也會(huì)消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗描沟,應(yīng)用應(yīng)當(dāng)盡量減少視圖數(shù)量和層次飒泻,并在不透明的視圖里標(biāo)明 opaque 屬性以避免無(wú)用的 Alpha 通道合成。當(dāng)然吏廉,這也可以用上面的方法泞遗,把多個(gè)視圖預(yù)先渲染為一張圖片來(lái)顯示。
圖形的生成席覆。
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ì)降到很低。為了避免這種情況遂庄,可以嘗試開(kāi)啟 CALayer.shouldRasterize 屬性寥院,但這會(huì)把原本離屏渲染的操作轉(zhuǎn)嫁到 CPU 上去。對(duì)于只需要圓角的某些場(chǎng)合涛目,也可以用一張已經(jīng)繪制好的圓角圖片覆蓋到原本視圖上面來(lái)模擬相同的視覺(jué)效果秸谢。最徹底的解決辦法凛澎,就是把需要顯示的圖形在后臺(tái)線程繪制為圖片,避免使用圓角估蹄、陰影塑煎、遮罩等屬性。
微博 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 來(lái)說(shuō)拓诸,提前在后臺(tái)計(jì)算好布局結(jié)果是非常重要的一個(gè)性能優(yōu)化點(diǎn)侵佃。為了達(dá)到最高性能,你可能需要犧牲一些開(kāi)發(fā)速度奠支,不要用 Autolayout 等技術(shù)馋辈,少用 UILabel 等文本控件。但如果你對(duì)性能的要求并不那么高倍谜,可以嘗試用 TableView 的預(yù)估高度的功能迈螟,并把每個(gè) Cell 高度緩存下來(lái)。這里有個(gè)來(lái)自百度知道團(tuán)隊(duì)的開(kāi)源項(xiàng)目可以很方便的幫你實(shí)現(xiàn)這一點(diǎn):FDTemplateLayoutCell尔崔。
預(yù)渲染
微博的頭像在某次改版中換成了圓形答毫,所以我也跟進(jìn)了一下。當(dāng)頭像下載下來(lái)后季春,我會(huì)在后臺(tái)線程將頭像預(yù)先渲染為圓形并單獨(dú)保存到一個(gè) ImageCache 中去洗搂。
對(duì)于 TableView 來(lái)說(shuō),Cell 內(nèi)容的離屏渲染會(huì)帶來(lái)較大的 GPU 消耗。在 Twitter Demo 中耘拇,我為了圖省事兒用到了不少 layer 的圓角屬性撵颊,你可以在低性能的設(shè)備(比如 iPad 3)上快速滑動(dòng)一下這個(gè)列表,能感受到雖然列表并沒(méi)有較大的卡頓惫叛,但是整體的平均幀數(shù)降了下來(lái)倡勇。用 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ú)提取出來(lái)葫笼,放到了這里: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)速度過(guò)快時(shí),繪制任務(wù)還沒(méi)有完成就可能已經(jīng)被取消了迁客。如果這時(shí)仍然繼續(xù)繪制郭宝,就會(huì)造成大量的 CPU 資源浪費(fèi),甚至阻塞線程并造成后續(xù)的繪制任務(wù)遲遲無(wú)法完成掷漱。我的做法是盡量快速粘室、提前判斷當(dāng)前繪制任務(wù)是否已經(jīng)被取消;在繪制每一行文本前卜范,我都會(huì)調(diào)用 isCancelled() 來(lái)進(jìn)行判斷衔统,保證被取消的任務(wù)能及時(shí)退出,不至于影響后續(xù)操作。
目前有些第三方微博客戶端(比如 VVebo缰冤、墨客等)犬缨,使用了一種方式來(lái)避免高速滑動(dòng)時(shí) Cell 的繪制過(guò)程,相關(guān)實(shí)現(xiàn)見(jiàn)這個(gè)項(xiàng)目:VVeboTableViewDemo棉浸。它的原理是怀薛,當(dāng)滑動(dòng)時(shí),松開(kāi)手指后迷郑,立刻計(jì)算出滑動(dòng)停止時(shí) Cell 的位置枝恋,并預(yù)先繪制那個(gè)位置附近的幾個(gè) Cell,而忽略當(dāng)前滑動(dòng)中的 Cell嗡害。這個(gè)方法比較有技巧性焚碌,并且對(duì)于滑動(dòng)性能來(lái)說(shuō)提升也很大,唯一的缺點(diǎn)就是快速滑動(dòng)中會(huì)出現(xiàn)大量空白內(nèi)容霸妹。如果你不想實(shí)現(xiàn)比較麻煩的異步繪制但又想保證滑動(dòng)的流暢性十电,這個(gè)技巧是個(gè)不錯(cuò)的選擇。
全局并發(fā)控制
當(dāng)我用 concurrent queue 來(lái)執(zhí)行大量繪制任務(wù)時(shí)叹螟,偶爾會(huì)遇到這種問(wèn)題:
大量的任務(wù)提交到后臺(tái)隊(duì)列時(shí)鹃骂,某些任務(wù)會(huì)因?yàn)槟承┰颍ù颂幨?CGFont 鎖)被鎖住導(dǎo)致線程休眠,或者被阻塞罢绽,concurrent queue 隨后會(huì)創(chuàng)建新的線程來(lái)執(zhí)行其他任務(wù)畏线。當(dāng)這種情況變多時(shí),或者 App 中使用了大量 concurrent queue 來(lái)執(zhí)行較多任務(wù)時(shí)良价,App 在同一時(shí)刻就會(huì)存在幾十個(gè)線程同時(shí)運(yùn)行寝殴、創(chuàng)建、銷毀明垢。CPU 是用時(shí)間片輪轉(zhuǎn)來(lái)實(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 過(guò)多曼验,并且非趁谏洌快速的滑動(dòng)時(shí),界面仍然會(huì)出現(xiàn)少量卡頓鬓照,我謹(jǐn)慎的猜測(cè)可能與這個(gè)問(wèn)題有關(guān)熔酷。
使用 concurrent queue 時(shí)不可避免會(huì)遇到這種問(wèn)題,但使用 serial queue 又不能充分利用多核 CPU 的資源豺裆。我寫(xiě)了一個(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í)行,這樣盡量避免了過(guò)多線程導(dǎo)致的性能問(wèn)題园匹。
更高效的異步圖片加載
SDWebImage 在這個(gè) Demo 里仍然會(huì)產(chǎn)生少量性能問(wèn)題雳刺,并且有些地方不能滿足我的需求,所以我自己實(shí)現(xiàn)了一個(gè)性能更高的圖片加載庫(kù)裸违。在顯示簡(jiǎn)單的單張圖片時(shí)掖桦,利用 UIView.layer.contents 就足夠了,沒(méi)必要使用 UIImageView 帶來(lái)額外的資源消耗供汛,為此我在 CALayer 上添加了 setImageWithURL 等方法枪汪。除此之外,我還把圖片解碼等操作通過(guò) YYDispatchQueuePool 進(jìn)行管理紊馏,控制了 App 總線程數(shù)量料饥。
其他可以改進(jìn)的地方
上面這些優(yōu)化做完后,微博 Demo 已經(jīng)非常流暢了朱监,但在我的設(shè)想中,仍然有一些進(jìn)一步優(yōu)化的技巧原叮,但限于時(shí)間和精力我并沒(méi)有實(shí)現(xiàn)赫编,下面簡(jiǎn)單列一下:
列表中有不少視覺(jué)元素并不需要觸摸事件,這些元素可以用 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ù)劃分為足夠小的塊问芬,并通過(guò) Runloop 來(lái)進(jìn)行調(diào)度,在每個(gè) Loop 里判斷下一次 VSync 的時(shí)間寿桨,并在下次 VSync 到來(lái)前此衅,把當(dāng)前未執(zhí)行完的任務(wù)延遲到下一個(gè)機(jī)會(huì)去。這個(gè)只是我的一個(gè)設(shè)想,并不一定能實(shí)現(xiàn)或起作用挡鞍。
如何評(píng)測(cè)界面的流暢度
最后還是要提一下骑歹,“過(guò)早的優(yōu)化是萬(wàn)惡之源”,在需求未定墨微,性能問(wèn)題不明顯時(shí)道媚,沒(méi)必要嘗試做優(yōu)化,而要盡量正確的實(shí)現(xiàn)功能欢嘿。做性能優(yōu)化時(shí)衰琐,也最好是走修改代碼 -> Profile -> 修改代碼這樣一個(gè)流程,優(yōu)先解決最值得優(yōu)化的地方炼蹦。
如果你需要一個(gè)明確的 FPS 指示器羡宙,可以嘗試一下 KMCGeigerCounter。對(duì)于 CPU 的卡頓掐隐,它可以通過(guò)內(nèi)置的 CADisplayLink 檢測(cè)出來(lái)狗热;對(duì)于 GPU 帶來(lái)的卡頓,它用了一個(gè) 1×1 的 SKView 來(lái)進(jìn)行監(jiān)視虑省。這個(gè)項(xiàng)目有兩個(gè)小問(wèn)題:SKView 雖然能監(jiān)視到 GPU 的卡頓匿刮,但引入 SKView 本身就會(huì)對(duì) CPU/GPU 帶來(lái)額外的一點(diǎn)的資源消耗;這個(gè)項(xiàng)目在 iOS 9 下有一些兼容問(wèn)題探颈,需要稍作調(diào)整熟丸。
我自己也寫(xiě)了個(gè)簡(jiǎn)單的 FPS 指示器:FPSLabel 只有幾十行代碼,僅用到了 CADisplayLink 來(lái)監(jiān)視 CPU 的卡頓問(wèn)題伪节。雖然不如上面這個(gè)工具完善光羞,但日常使用沒(méi)有太大問(wèn)題。
最后怀大,用 Instuments 的 GPU Driver 預(yù)設(shè)纱兑,能夠?qū)崟r(shí)查看到 CPU 和 GPU 的資源消耗。在這個(gè)預(yù)設(shè)內(nèi)化借,你能查看到幾乎所有與顯示有關(guān)的數(shù)據(jù)潜慎,比如 Texture 數(shù)量、CA 提交的頻率蓖康、GPU 消耗等铐炫,在定位界面卡頓的問(wèn)題時(shí),這是最好的工具钓瞭。