如何讓iOS 保持界面流暢肯污?這些技巧你知道嗎
如何讓iOS 保持界面流暢?這些技巧你知道嗎
作者:ibireme這篇文章會非常詳細(xì)的分析 iOS 界面構(gòu)建中的各種性能問題以及對應(yīng)的解決思路吨枉,同時給出一個開源的微博列表實現(xiàn)蹦渣,通過實際的代碼展示如何構(gòu)建流暢的交互。
Index
1.演示項目
2.屏幕顯示圖像的原理
3.卡頓產(chǎn)生的原因和解決方案
CPU 資源消耗原因和解決方案
GPU 資源消耗原因和解決方案
4.AsyncDisplayKit
ASDK 的由來
ASDK 的資料
ASDK 的基本原理
ASDK 的圖層預(yù)合成
ASDK 異步并發(fā)操作
Runloop 任務(wù)分發(fā)
5.微博 Demo 性能優(yōu)化技巧
預(yù)排版
預(yù)渲染
異步繪制
全局并發(fā)控制
更高效的異步圖片加載
其他可以改進(jìn)的地方
6.如何評測界面的流暢度
演示項目
在開始技術(shù)討論前貌亭,你可以先下載我寫的 Demo 跑到真機上體驗一下: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è)備大都是液晶顯示屏了糊闽,但原理仍然沒有變。
通常來說爹梁,計算機系統(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ū)川慌,即雙緩沖機制啦粹。在這種情況下偿荷,GPU 會預(yù)先渲染好一幀放入一個緩沖區(qū)內(nèi),讓視頻控制器讀取唠椭,當(dāng)下一幀渲染好后,GPU 會直接把視頻控制器的指針指向第二個緩沖器忍饰。如此一來效率會有很大的提升贪嫂。
雙緩沖雖然能解決效率問題,但會引入一個新的問題艾蓝。當(dāng)視頻控制器還未讀取完成時力崇,即屏幕內(nèi)容剛顯示一半時,GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個緩沖區(qū)進(jìn)行交換后赢织,視頻控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上亮靴,造成畫面撕裂現(xiàn)象,如下圖:
為了解決這個問題于置,GPU 通常有一個機制叫做垂直同步(簡寫也是 V-Sync)茧吊,當(dāng)開啟垂直同步后,GPU 會等待顯示器的 VSync 信號發(fā)出后八毯,才進(jìn)行新的一幀渲染和緩沖區(qū)更新搓侄。這樣能解決畫面撕裂現(xiàn)象,也增加了畫面流暢度话速,但需要消費更多的計算資源讶踪,也會帶來部分延遲。
那么目前主流的移動設(shè)備是什么情況呢泊交?從網(wǎng)上查到的資料可以知道乳讥,iOS 設(shè)備會始終使用雙緩存,并開啟垂直同步廓俭。而安卓設(shè)備直到 4.1 版本云石,Google 才開始引入這種機制,目前安卓系統(tǒng)是三緩存+垂直同步白指。
卡頓產(chǎn)生的原因和解決方案
在 VSync 信號到來后留晚,系統(tǒng)圖形服務(wù)會通過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內(nèi)容告嘲,比如視圖的創(chuàng)建错维、布局計算、圖片解碼橄唬、文本繪制等赋焕。隨后 CPU 會將計算好的內(nèi)容提交到 GPU 去,由 GPU 進(jìn)行變換仰楚、合成隆判、渲染犬庇。隨后 GPU 會把渲染結(jié)果提交到幀緩沖區(qū)去,等待下一次 VSync 信號到來時顯示到屏幕上侨嘀。由于垂直同步的機制臭挽,如果在一個 VSync 時間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交咬腕,則那一幀就會被丟棄欢峰,等待下一次機會再顯示,而這時顯示屏?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ā)生在主線程的琴许,并且不可避免粗俱。如果想要繞開這個機制,常見的做法是在后臺線程先把圖片繪制到 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 以上機型造锅,紋理尺寸上限都是 4096x4096撼唾,更詳細(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)部的一個中間變量哩俭,稍后在需要時,再通過某個機制一次性設(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 真機上通常是 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 等機制多次觸發(fā)相關(guān)流程恢氯。
ASDK 在此處模擬了 Core Animation 的這個機制:所有針對 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幔虏。
通過這種機制纺念,ASDK 可以在合適的機會把異步、并發(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ù)延遲到下一個機會去钠四。這個只是我的一個設(shè)想盗扒,并不一定能實現(xiàn)或起作用。
如何評測界面的流暢度
最后還是要提一下缀去,“過早的優(yōu)化是萬惡之源”侣灶,在需求未定,性能問題不明顯時缕碎,沒必要嘗試做優(yōu)化褥影,而要盡量正確的實現(xiàn)功能。做性能優(yōu)化時咏雌,也最好是走修改代碼 -> Profile -> 修改代碼這樣一個流程凡怎,優(yōu)先解決最值得優(yōu)化的地方。
如果你需要一個明確的 FPS 指示器赊抖,可以嘗試一下 KMCGeigerCounter统倒。對于 CPU 的卡頓,它可以通過內(nèi)置的 CADisplayLink 檢測出來氛雪;對于 GPU 帶來的卡頓房匆,它用了一個 1x1 的 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 消耗等服猪,在定位界面卡頓的問題時供填,這是最好的工具。
南來地,北往的,上班的,下崗的,走過路過不要錯過!
======================個性簽名=====================
之前認(rèn)為Apple 的iOS 設(shè)計的要比 Android 穩(wěn)定,我錯了嗎?
下載的許多客戶端程序/游戲程序,經(jīng)常會Crash,是程序?qū)懙牟缓?內(nèi)存泄漏?剛啟動也會嗎?)還是iOS本身的不穩(wěn)定!!!
如果在Android手機中可以簡單聯(lián)接到ddms,就可以查看系統(tǒng)log,很容易看到程序為什么出錯,在iPhone中如何得知呢?試試Organizer吧,分析一下Device logs,也許有用.