這是 Core Animation 的系列文章,介紹了 Core Animation 的用法淌友,以及如何進(jìn)行性能優(yōu)化煌恢。
前面的幾篇文章已經(jīng)介紹了 Core Animation 的動(dòng)畫、圖層拨拓、時(shí)間等肴颊,Core Animation 功能和性能都很強(qiáng)大。但如果不清楚核心動(dòng)畫原理渣磷,可能會以非常低效的方式使用婿着。
這篇文章介紹可能引起動(dòng)畫性能降低的原因,以及如何避免幸海、修復(fù)這些問題祟身。
1. CPU VS GPU
渲染和動(dòng)畫涉及到CPU(中央處理器,Central Processing Unit)和GPU(Graphics Processing Unit)物独。在現(xiàn)代的 iOS 設(shè)備中袜硫,CPU和GPU都是可編程芯片,即可運(yùn)行不同軟件挡篓。但由于歷史原因婉陷,常稱 CPU 執(zhí)行的工作為軟件層面,GPU 為硬件層面官研。
GPU 最初旨在為計(jì)算機(jī)圖形和視頻創(chuàng)建圖像秽澳,但自2010年以來,GPU 也可用于加速涉及大量數(shù)據(jù)的計(jì)算戏羽。
可以將 CPU 視為整個(gè)系統(tǒng)的任務(wù)負(fù)責(zé)人担神,協(xié)調(diào)各種通用計(jì)算任務(wù)。GPU 執(zhí)行較窄領(lǐng)域的更專門的任務(wù)(通常是數(shù)學(xué)任務(wù))始花。借助并發(fā)妄讯,GPU 執(zhí)行任務(wù)速度遠(yuǎn)遠(yuǎn)高于 CPU孩锡。
CPU、GPU 對比如下:
CPU | GPU |
---|---|
多核 | 非常多核 |
低延遲 | 高通量 |
適合串行任務(wù) | 適合并行任務(wù) |
可以同時(shí)執(zhí)行多項(xiàng)任務(wù) | 可以同時(shí)執(zhí)行上千任務(wù) |
通常來講可以在 CPU 中執(zhí)行所有任務(wù)亥贸,但對于圖像處理 GPU 速度會更快躬窜。因?yàn)閳D像處理涉及大量浮點(diǎn)計(jì)算,而 GPU 更擅長此類計(jì)算炕置。因此荣挨,將盡可能多的渲染工作轉(zhuǎn)移給 GPU 處理,但如果將過多任務(wù)交由 GPU 處理朴摊,也會影響 app 性能默垄。
性能優(yōu)化就是將任務(wù)合理分配給 CPU、GPU甚纲。為了實(shí)現(xiàn)這一目標(biāo)厕倍,需先了解 Core Animation 如何為 CPU、GPU 分配任務(wù)贩疙。
1.1 動(dòng)畫階段
動(dòng)畫和組合圖層由單獨(dú)進(jìn)程處理,獨(dú)立于 app 之外况既,該進(jìn)程被稱為 render tree这溅。
提交交易 Commit Transaction時(shí),分為以下四個(gè)階段:
- 布局 layout:準(zhǔn)備 view棒仍、layer 層級結(jié)構(gòu)悲靴,設(shè)置 layer 屬性,例如:
frame
莫其、backgroundColor
癞尚、border
等。 - 渲染 display:對 layer 的 backing image 進(jìn)行渲染乱陡。渲染會調(diào)用
draw(_:)
浇揩、draw(_:in:)
方法。 - 準(zhǔn)備 prepare:Core Animation 準(zhǔn)備將 animation data 發(fā)送至 render server憨颠。圖片解碼胳徽、轉(zhuǎn)換也是在此階段。
- 提交 commit:Core Animation 打包所有圖層和屬性爽彤,使用進(jìn)程間通信(IPC养盗,Inter-Process Communication)發(fā)送至 render tree 進(jìn)行渲染。這個(gè)過程是遞歸的适篙,必須迭代整個(gè)視圖層級往核。如果圖層樹復(fù)雜,整個(gè)過程會很昂貴嚷节。這也是為什么要讓圖層樹盡可能平坦聂儒,以確保這一階段盡可能高效虎锚。
上述四個(gè)階段發(fā)生在 app 內(nèi),在顯示到屏幕上前還有額外工作薄货。當(dāng)打包的 layer 和動(dòng)畫到達(dá) render server 進(jìn)程時(shí)翁都,會被反序列化為圖層樹,稱為 render tree谅猾。借助 render tree 為每一幀動(dòng)畫執(zhí)行以下工作:
為所有圖層屬性計(jì)算中間值柄慰,設(shè)置 OpenGL/Metal 幾何形狀執(zhí)行渲染。
在屏幕上渲染可見的三角形税娜。
所以坐搔,共有六個(gè)階段。最后兩個(gè)階段在動(dòng)畫過程中不停重復(fù)敬矩,前五個(gè)階段由 CPU 執(zhí)行概行,最后一個(gè)階段由 GPU 執(zhí)行。你只能控制前兩個(gè)階段:布局和繪制弧岳,Core Animation 框架內(nèi)部處理剩余階段凳忙。
動(dòng)畫本身分為三個(gè)階段:
- 創(chuàng)建動(dòng)畫组去,更新視圖層級只壳。
- 準(zhǔn)備和提交動(dòng)畫。這一階段會調(diào)用
layoutSubviews()
和drawRect
圾浅。 - 提交視圖層級和動(dòng)畫腹尖。
在布局和渲染階段柳恐,你可以決定哪些工作交由 CPU、哪些工作交由 GPU热幔。但應(yīng)如何分配呢乐设?
1.2 GPU 相關(guān)操作
GPU 為特定任務(wù)進(jìn)行了優(yōu)化,采集圖片绎巨、集合信息近尚,運(yùn)行變換,應(yīng)用紋理认烁,混合后輸出到屏幕上肿男。GPU 在這些操作上有很大的靈活性,但 Core Animation 并未提供相關(guān)接口却嗡。除非繞過 Core Animation 直接使用 OpenGL/Metal舶沛,否則,可使用硬件加速的的任務(wù)非常固定窗价。
從廣義上講如庭,大部分CALayer
的屬性使用 GPU 渲染。例如,背景色坪它、描邊色可以使用著色的三角形高效繪制骤竹。為contents
屬性設(shè)置圖片,即使進(jìn)行了縮放往毡、裁剪蒙揣,也可以使用紋理三角形高效渲染,無需使用 CPU开瞭。
但以下這些操作會降低 GPU 渲染性能:
- 太多幾何結(jié)構(gòu)懒震。GPU 可以處理百萬級三角板,幾何結(jié)構(gòu)一般不會是核心動(dòng)畫的瓶頸嗤详,但 layer 需經(jīng)過預(yù)處理后使用 IPC 發(fā)送至 render tree个扰。太多圖層會引起 CPU 性能瓶頸,進(jìn)而影響一次可渲染圖層數(shù)量葱色。
- 重繪(Overdraw)递宅。主要由半透明圖層重疊造成,GPU 的填充比率(fill rate苍狰,用顏色填充像素的比率)是有限的办龄,所以需避免重繪(同一幀用相同的像素填充多次)。GPU 已經(jīng)可以應(yīng)對重繪淋昭,即使是 iPhone 3GS土榴,也可以處理2.5的填充率,同時(shí)保持60fps响牛。即可以渲染2.5屏內(nèi)容。新設(shè)備重繪能力更為強(qiáng)勁赫段。
- 離屏渲染(Offscreen drawing)呀打。某些效果不能直接向屏幕渲染,而需先在離屏 context 渲染糯笙。Offscreen drawing 發(fā)生于 GPU 渲染的 render server贬丛,此時(shí)需要為離屏圖片上下文開辟額外內(nèi)存空間,并進(jìn)行繪制上下文切換给涕,這些都會降低 GPU 性能豺憔。
cornerRadius
、mask
够庙、陰影恭应、柵格化都會導(dǎo)致 Core Animation 離屏預(yù)渲染圖層。這并不是說需要禁止使用這些效果耘眨,只是使用時(shí)需了解其對性能的影響昼榛。 - 過大的圖片。如果渲染的圖片大于 GPU 紋理(texture)支持的最大大刑弈选(通常為2048*2048或4096*4096胆屿,隨硬件改變)奥喻,每次渲染前都需要使用 CPU 進(jìn)行預(yù)處理,這會降低 GPU 性能非迹。
1.3 CPU 相關(guān)操作
Core Animation 動(dòng)畫開始前环鲤,CPU 已經(jīng)開始處理相關(guān)工作了。這意味著不會影響幀率憎兽,但可能導(dǎo)致動(dòng)畫延遲冷离,進(jìn)而導(dǎo)致界面無響應(yīng)。
下面這些 CPU 操作均會延遲動(dòng)畫開始時(shí)間:
- 計(jì)算布局唇兑。如果視圖層級結(jié)構(gòu)復(fù)雜酒朵,當(dāng)顯示、修改視圖時(shí)扎附,會需要一定時(shí)間計(jì)算布局蔫耽。
- 視圖懶加載。只有在屏幕上顯示視圖控制器時(shí)留夜,系統(tǒng)才會加載所需視圖。這樣有利于減少內(nèi)存、降低 app 啟動(dòng)時(shí)間嚼摩,但如果顯示到屏幕上前需要處理太多工作钦讳,會導(dǎo)致 app 無響應(yīng)。如果控制器數(shù)據(jù)來自于本地?cái)?shù)據(jù)庫枕面,控制器來自于 storyboard愿卒,或者包含圖片等,都會導(dǎo)致磁盤讀寫潮秘,磁盤讀寫比 CPU 指令慢很多琼开。
- Core Graphics 繪制柜候。如果實(shí)現(xiàn)了視圖的
draw(_:)
方法,或CALayerDelegate
協(xié)議的draw(_:in:)
方法躏精,即使不繪制任何內(nèi)容飞主,也會產(chǎn)生明顯性能開銷。為了支持繪制內(nèi)容,Core Animation 必須在內(nèi)存中開辟與視圖同大小的 backing image碌识,繪制完成后使用進(jìn)程間通信將數(shù)據(jù)發(fā)送給 render server碾篡。相對來說,Core Graphics 繪制速度慢筏餐,不推薦在性能挑剔場景使用开泽。 - 圖片解碼 Image decompression。編碼的 png魁瞪、jpeg圖片比未編碼圖片小很多穆律。圖片顯示到屏幕前,必須先解碼导俘。為了減少內(nèi)存占用峦耘,iOS 直到需要顯示圖片時(shí)才對圖片進(jìn)行解碼。如果是大圖旅薄,解碼過程可能很耗時(shí)辅髓。
圖層打包發(fā)送到 render tree 后,CPU 還需繼續(xù)處理動(dòng)畫相關(guān)工作少梁。為了把圖層顯示到屏幕洛口,Core Animation 需循環(huán) render tree 中可見圖層,并轉(zhuǎn)換為紋理三角板以便 OpenGL/Metal 處理凯沪。CPU 必須處理這些轉(zhuǎn)換第焰,因?yàn)?GPU 對圖層層級結(jié)構(gòu)沒有概念。CPU 在這一環(huán)節(jié)工作量與圖層數(shù)量成正比妨马,圖層多了后 CPU 渲染每一幀的負(fù)擔(dān)都會增加挺举,盡管這些工作在 app 之外的進(jìn)程執(zhí)行。
1.4 IO 相關(guān)操作
IO(Input/Output烘跺,即輸入/輸出)指通過磁盤豹悬、網(wǎng)絡(luò)接口輸入、輸出數(shù)據(jù)液荸。有些動(dòng)畫可能涉及從磁盤讀取數(shù)據(jù),例如脱篙,切換控制器時(shí)從磁盤讀區(qū) storyboard娇钱、nib文件;輪播圖片太大時(shí)绊困,每次都需要從磁盤讀取文搂。
IO 比訪問內(nèi)存慢很多。如果動(dòng)畫過程中涉及 IO秤朗,動(dòng)畫性能可能會出現(xiàn)問題煤蹭。可以借助線程、緩存硝皂、預(yù)加載技術(shù)解決 IO 問題常挚。
2. 測量 而非猜測
已經(jīng)掌握了動(dòng)畫可能出現(xiàn)性能瓶頸的點(diǎn),但如何解決這些性能瓶頸呢稽物?一般奄毡,借助工具測量性能問題出在哪里,而非根據(jù)猜測嘗試修復(fù)贝或。
2.1 在真機(jī)測量
進(jìn)行性能優(yōu)化時(shí)吼过,應(yīng)在真機(jī)測試,而非模擬器咪奖。模擬器可以提高開放效率盗忱,但并不能準(zhǔn)確反映設(shè)備性能。
模擬器運(yùn)行于 Mac 上羊赵,Mac 的 CPU 可能比 iOS 設(shè)備 CPU 性能高很多趟佃。Mac 的 GPU 與 iOS 的有很多不同,Mac 需要使用 CPU 模擬 GPU慷垮,導(dǎo)致模擬器中 GPU 相關(guān)操作比 CPU 中的慢很多揖闸,特別使用了CAEAGLLayer
時(shí)。也就是模擬器中的性能表現(xiàn)和真機(jī)可能有很大不同料身。
測試性能的另一要點(diǎn)就是使用發(fā)布版本(Release)汤纸,而非調(diào)試版(Debug mode)。構(gòu)建發(fā)布版時(shí)芹血,編譯器會過濾掉調(diào)試內(nèi)容贮泞,包括開發(fā)者自定義的在 release 版本中移除print
語句等,我們只關(guān)注 release 版本性能幔烛,也只需在 release 中測試性能啃擦。
最后,盡可能在 app 支持的最舊的設(shè)備中測試饿悬。有時(shí)令蛉,Apple 發(fā)布的大版本系統(tǒng)更新、硬件發(fā)布狡恬,相關(guān)性能可能發(fā)生變化珠叔,最好能夠在不同設(shè)備、不同版本系統(tǒng)中進(jìn)行測試弟劲。
2.2 保持一致的幀率
為了獲得平滑動(dòng)畫祷安,動(dòng)畫幀率應(yīng)與屏幕刷新幀率一致。使用CADisplayLink可以降低幀率到30fps兔乞,但沒有辦法修改 Core Animaiton 動(dòng)畫的幀率汇鞭。如果動(dòng)畫達(dá)不到60fps凉唐,會有幀會被跳過,app 產(chǎn)生卡頓霍骄。
眼睛可以分辨出 app 是否有嚴(yán)重掉幀台囱,但不能衡量出嚴(yán)重程度。想要優(yōu)化性能腕巡,需要獲得當(dāng)前幀率數(shù)值玄坦。Xcode 提供的 Instruments 工具可以檢測幀率。
3. Instruments 介紹
Instruments 是 Xcode 的檢測工具绘沉,可以檢測應(yīng)用啟動(dòng)時(shí)間煎楣、動(dòng)畫、電量消耗车伞、內(nèi)存泄漏等择懂。
3.1 Time Profiler 模版
使用 Time Profiler 監(jiān)測 CPU 使用率,可以獲知哪個(gè)方法占用了大量 CPU 時(shí)間另玖。雖然使用大量 CPU 不一定是性能瓶頸困曙,但在動(dòng)畫出現(xiàn)性能問題時(shí),你可能需要判斷是否是 CPU 問題導(dǎo)致的谦去。
Time Profiler 的 Call Tree 有一些選項(xiàng)慷丽,用于過濾掉不關(guān)心的內(nèi)容。常用選項(xiàng)如下:
- Separate by State:根據(jù) app 生命周期狀態(tài)分組鳄哭,可以方便看到 app 各狀態(tài)下的工作量要糊。
- Separate by Thread:根據(jù)方法所在線程進(jìn)行分組。如果使用了多線程妆丘,Separate by thread 可以區(qū)分出那個(gè)線程導(dǎo)致的問題锄俄。
- Invert Call Tree:勾選后,將反轉(zhuǎn)堆棧勺拣,先顯示底部部分奶赠。
- Hide System Libraries:隱藏所有系統(tǒng)框架方法,可以區(qū)分出我們的那個(gè)方法有性能瓶頸药有。
- Flatten Recursive:將遞歸調(diào)用合并到一個(gè)單個(gè)條目中毅戈。
- Top Functions:調(diào)用函數(shù)所用時(shí)間,與從該函數(shù)調(diào)用的函數(shù)消耗的時(shí)間相累加愤惰,有助于找到耗時(shí)方法苇经。
3.2 Core Animation 模版
Instruments 的 Core Animation 工具用于監(jiān)測核心動(dòng)畫性能,可以記錄動(dòng)畫幀率
3.3 Xcode Rendering 選項(xiàng)
Xcode 提供的 Rendering 選項(xiàng)羊苟,用于輔助調(diào)試渲染性能瓶頸。點(diǎn)擊Xcode 中的 Debug > View Debugging > Rendering 可以看到所有選項(xiàng)感憾。
- Color Blended Layers:高亮顯示屏幕中半透明圖層重疊的區(qū)域蜡励,并根據(jù)重疊程度由綠色變?yōu)榧t色令花。重疊會導(dǎo)致重繪,進(jìn)而影響 GPU 性能凉倚,也是滑動(dòng)兼都、動(dòng)畫性能下降的常見原因。
- Color Hits Green and Misses Red:開啟
shouldRasterize
屬性后稽寒,layer 繪制會被緩存起來扮碧,并被渲染成一張圖片。如果緩存的圖片需要不斷重新生成杏糙,該選項(xiàng)會使用紅色標(biāo)記重繪部分慎王,這一部分對性能會有負(fù)面影響。 - Color Copied Images:如果圖片色彩格式 GPU 不支持宏侍,就需要先在 CPU 中轉(zhuǎn)換赖淤。Core Animation 就需要復(fù)制一份圖片,而非傳遞指針谅河,Color copied Images 使用藍(lán)色標(biāo)記圖片咱旱。復(fù)制圖片對內(nèi)存消耗很大,應(yīng)盡可能避免绷耍。
- Color Immediatelly:正常情況下吐限,Instruments 的 Core Animation 模版10毫秒更新一次調(diào)試色。對于某些特殊情況褂始,你可能想獲得調(diào)試色的立即更新诸典,但開啟 Color Immediately 后可能影響渲染性能、動(dòng)畫幀率病袄,一般無需長期開啟搂赋。
- Color Misaligned Images:高亮顯示被拉伸、壓縮益缠,或邊界未與像素對齊(即寬高不是整數(shù))的圖片脑奠。大部分情況下對圖片進(jìn)行壓縮、拉伸很正常幅慌,但如果錯(cuò)誤的把大圖作為縮略圖使用宋欺,可以使用 Color Misaligned Images 檢測發(fā)現(xiàn)。
- Color Offscreen-Rendered Yellow:使用黃色高亮顯示離屏渲染的圖層胰伍,可以使用
shouldRasterize
齿诞、shadowPath
優(yōu)化圖層。 - Flash Updated Regions:使用黃色高亮顯示重繪部分骂租,即任何使用 Core Graphics 重繪的圖層祷杈。這種繪制速度慢,如果高頻重繪會影響應(yīng)用性能渗饮,可以通過使用緩存或其他技術(shù)解決但汞。
此外宿刮,模擬器 Debug 選項(xiàng)中也提供了部分高亮選項(xiàng)。
4. 使用 Instruments 優(yōu)化性能
在這一部分使用 Instruments 分析私蕾、優(yōu)化應(yīng)用性能僵缺。
Demo 使用UITableView
展示通訊錄。為了模擬真實(shí)效果踩叭,使用UIImage(contentsOfFile:)
加載圖片磕潮,而沒有使用會使用緩存的UIImage(named:)
方法加載頭像,還添加了陰影容贝,以便 app 性能更低自脯。
如下所示:
/// 使用 Instruments 的 Core Animation 模版監(jiān)測性能
class PerformanceViewController: BaseViewController {
var items = [Dictionary<String, Any>]()
var tableView = UITableView(frame: .zero, style: .plain)
override func viewDidLoad() {
super.viewDidLoad()
var array = [Dictionary<String, Any>]()
for _ in 0...3000 {
array.append(["name": self.randomName(), "image": self.randomAvatar()])
}
items = array
tableView.backgroundColor = .white
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
private func randomName() -> String {
let first = ["Alice", "Bill", "Charles", "Dan", "Dave", "Ethan", "Frank"]
let last = ["Appleseed", "Bandicoot", "Caravan", "Dabble", "Ernest", "Fortune"]
let index1 = arc4random_uniform(UInt32(first.count))
let index2 = arc4random_uniform(UInt32(last.count))
return String(first[Int(index1)] + last[Int(index2)])
}
private func randomAvatar() -> String {
let images = ["Man", "Igloo", "Cone", "Spaceship", "Anchor", "Key"]
let idx = arc4random_uniform(UInt32(images.count))
return images[Int(idx)]
}
}
extension PerformanceViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as UITableViewCell
let dict: Dictionary = items[indexPath.row]
let filePath = Bundle.main.path(forResource: dict["image"] as? String, ofType: "png")
cell.imageView?.image = UIImage(contentsOfFile: filePath ?? "")
cell.textLabel?.text = dict["name"] as? String
// set image shadow
cell.imageView?.layer.shadowOffset = CGSize(width: 0, height: 5)
cell.imageView?.layer.shadowOpacity = 0.75
cell.clipsToBounds = true
// set text shadow
cell.textLabel?.backgroundColor = UIColor.clear
cell.textLabel?.layer.shadowOffset = CGSize(width: 0, height: 2)
cell.textLabel?.layer.shadowOpacity = 0.5
return cell
}
}
快速滑動(dòng) table view,可以明顯感受到掉幀嗤疯。
在 Xcode 中使用 command + I 快捷鍵 Profile 當(dāng)前demo冤今,選取 Instruments 中的 Core Animation 模版,分析后如下:
目前茂缚,滑動(dòng)表視圖時(shí)幀率約14FPS戏罢。
開啟 Xcode 中的 Color Blended Layers 選項(xiàng),運(yùn)行后如下:
紅色區(qū)域?yàn)閳D層重疊區(qū)域脚囊。
開啟 Color Offscreen-Rendered Yellow 選項(xiàng):
可以看到 table view 的 cell 是黃色的龟糕,這可能是圖片和文本添加陰影導(dǎo)致的。去掉文本悔耘、圖片陰影讲岁,黃色區(qū)域會消失,app 滑動(dòng)也會流暢起來衬以。
cell 中頭像缓艳、姓名文本不會在每一幀刷新時(shí)改變,可以使用shouldRasterize
屬性緩存圖層看峻,之后使用緩存的圖層阶淘,直到圖層改變需要更新。
更新tableView(_:cellForRowAt:)
方法互妓,在return cell
前添加以下代碼:
// rasterize
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
使用shouldRasterize
后溪窒,layer 仍然會進(jìn)行離屏渲染,但因?yàn)殚_啟了柵格化 rasterization冯勉,Core Animation 會緩存繪制結(jié)果澈蚌,陰影對性能影響也會變小。
使用 Instruments 的 Core Animation 模版分析應(yīng)用灼狰,幀率能達(dá)到58幀宛瞄。與之前相比,性能提升了很多交胚。
總結(jié)
這一篇文章介紹了 Core Animation 是如何渲染的份汗,以及可能出現(xiàn)性能瓶頸的點(diǎn)伐厌,還介紹了使用 Instruments、Xcode 檢測性能問題裸影。
Demo名稱:CoreAnimation
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/CoreAnimation
參考資料:
- How CPU and GPU Work Together
- What’s the Difference Between a CPU and a GPU?
- Instruments Tutorial with Swift: Getting Started
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/影響動(dòng)畫性能的因素及如何使用%20Instruments%20檢測.md