這一系列的文章會(huì)從幾個(gè)方面對(duì) ASDK 在性能調(diào)優(yōu)方面策略的實(shí)現(xiàn)進(jìn)行分析,幫助讀者理解 ASDK 如何做到使復(fù)雜的 UI 界面達(dá)到 60 FPS 的刷新頻率的候学;本篇文章會(huì)從視圖的渲染層面講解 ASDK 對(duì)于渲染過程的優(yōu)化并對(duì) ASDK 進(jìn)行概述。
在客戶端或者前端開發(fā)中悬钳,對(duì)于性能的優(yōu)化蕴掏,尤其是 UI冀瓦,往往都不是最先考慮的問題。
因?yàn)樵诖蠖鄶?shù)場(chǎng)景下丰辣,使用更加復(fù)雜的高性能代碼替代可用的代碼經(jīng)常會(huì)導(dǎo)致代碼的可維護(hù)性下降撒强,所以更需要我們開發(fā)者對(duì)優(yōu)化的時(shí)間點(diǎn)以及原因有一個(gè)比較清楚的認(rèn)識(shí),避免過度優(yōu)化帶來的問題笙什。
對(duì) iOS 開發(fā)比較熟悉的開發(fā)者都知道飘哨,iOS 中的性能問題大多是阻塞主線程導(dǎo)致用戶的交互反饋出現(xiàn)可以感知的延遲。
詳細(xì)說起來琐凭,大體有三種原因:
- UI 渲染需要時(shí)間較長芽隆,無法按時(shí)提交結(jié)果;
- 一些需要密集計(jì)算的處理放在了主線程中執(zhí)行统屈,導(dǎo)致主線程被阻塞胚吁,無法渲染 UI 界面;
- 網(wǎng)絡(luò)請(qǐng)求由于網(wǎng)絡(luò)狀態(tài)的問題響應(yīng)較慢愁憔,UI 層由于沒有模型返回?zé)o法渲染腕扶。
上面的這些問題都會(huì)影響應(yīng)用的性能,最常見的表現(xiàn)就是 UITableView
在滑動(dòng)時(shí)沒有達(dá)到 60 FPS吨掌,用戶能感受到明顯的卡頓蕉毯。
屏幕的渲染
相信點(diǎn)開這篇文章的大多數(shù)開發(fā)者都知道 FPS 是什么,那么如果才能優(yōu)化我們的 App 使其達(dá)到 60 FPS 呢思犁?在具體了解方法之前,我們先退一步进肯,提出另一個(gè)問題激蹲,屏幕是如何渲染的?
對(duì)于第一個(gè)問題江掩,可能需要幾篇文章來回答学辱,希望整個(gè)系列的文章能給你一個(gè)滿意的答案。3
CRT 和 LCD
屏幕的渲染可能要從 CRT(Cathode ray tube) 顯示器和 LCD(Liquid-crystal display) 顯示器講起环形。
CRT 顯示器是比較古老的技術(shù)策泣,它使用陰極電子槍發(fā)射電子,在陰極高壓的作用下抬吟,電子由電子槍射向熒光屏萨咕,使熒光粉發(fā)光,將圖像顯示在屏幕上火本,這也就是用磁鐵靠近一些老式電視機(jī)的屏幕會(huì)讓它們變色的原因危队。
而 FPS 就是 CRT 顯示器的刷新頻率聪建,電子槍每秒會(huì)對(duì)顯示器上內(nèi)容進(jìn)行 60 - 100 次的刷新,哪怕在我們看來沒有任何改變茫陆。
但是 LCD 的原理與 CRT 非常不同金麸,LCD 的成像原理跟光學(xué)有關(guān):
- 在不加電壓下,光線會(huì)沿著液晶分子的間隙前進(jìn)旋轉(zhuǎn) 90°簿盅,所以光可以通過挥下;
- 在加入電壓之后,光沿著液晶分子的間隙直線前進(jìn)桨醋,被濾光板擋住棚瘟。
如果你可以翻墻映之,相信下面的視頻會(huì)更好得幫助你理解 LCD 的工作原理:
<a ></a>
LCD 的成像原理雖然與 CRT 截然不同浪南,每一個(gè)像素的顏色可以在需要改變時(shí)才去改變電壓,也就是不需要刷新頻率挺尾,但是由于一些歷史原因返顺,LCD 仍然需要按照一定的刷新頻率向 GPU 獲取新的圖像用于顯示禀苦。
屏幕撕裂
但是顯示器只是用于將圖像顯示在屏幕上,誰又是圖像的提供者呢遂鹊?圖像都是我們經(jīng)常說的 GPU 提供的振乏。
而這導(dǎo)致了另一個(gè)問題,由于 GPU 生成圖像的頻率與顯示器刷新的頻率是不相關(guān)的秉扑,那么在顯示器刷新時(shí)慧邮,GPU 沒有準(zhǔn)備好需要顯示的圖像怎么辦;或者 GPU 的渲染速度過快舟陆,顯示器來不及刷新误澳,GPU 就已經(jīng)開始渲染下一幀圖像又該如何處理?
如果解決不了這兩個(gè)問題秦躯,就會(huì)出現(xiàn)上圖中的屏幕撕裂(Screen Tearing)現(xiàn)象忆谓,屏幕中一部分顯示的是上一幀的內(nèi)容,另一部分顯示的是下一幀的內(nèi)容踱承。
我們用兩個(gè)例子來說明可能出現(xiàn)屏幕撕裂的兩種情況:
- 如果顯示器的刷新頻率為 75 Hz倡缠,GPU 的渲染速度為 100 Hz,那么在兩次屏幕刷新的間隔中茎活,GPU 會(huì)渲染 4/3 個(gè)幀昙沦,后面的 1/3 幀會(huì)覆蓋已經(jīng)渲染好的幀棧,最終會(huì)導(dǎo)致屏幕在 1/3 或者 2/3 的位置出現(xiàn)屏幕撕裂效果载荔;
- 那么 GPU 的渲染速度小于顯示器呢盾饮,比如說 50 Hz,那么在兩次屏幕刷新的間隔中,GPU 只會(huì)渲染 2/3 幀丐谋,剩下的 1/3 會(huì)來自上一幀芍碧,與上面的結(jié)果完全相同,在同樣的位置出現(xiàn)撕裂效果号俐。
到這里泌豆,有人會(huì)說,如果顯示器的刷新頻率與 GPU 的渲染速度完全相同吏饿,應(yīng)該就會(huì)解決屏幕撕裂的問題了吧踪危?其實(shí)并不是。顯示器從 GPU 拷貝幀的過程依然需要消耗一定的時(shí)間猪落,如果屏幕在拷貝圖像時(shí)刷新贞远,仍然會(huì)導(dǎo)致屏幕撕裂問題。
引入多個(gè)緩沖區(qū)可以有效地緩解屏幕撕裂笨忌,也就是同時(shí)使用一個(gè)幀緩沖區(qū)(frame buffer)和多個(gè)后備緩沖區(qū)(back buffer)蓝仲;在每次顯示器請(qǐng)求內(nèi)容時(shí),都會(huì)從幀緩沖區(qū)中取出圖像然后渲染官疲。
雖然緩沖區(qū)可以減緩這些問題袱结,但是卻不能解決;如果后備緩沖區(qū)繪制完成途凫,而幀緩沖區(qū)的圖像沒有被渲染垢夹,后備緩沖區(qū)中的圖像就會(huì)覆蓋幀緩沖區(qū),仍然會(huì)導(dǎo)致屏幕撕裂维费。
解決這個(gè)問題需要另一個(gè)機(jī)制的幫助果元,也就是垂直同步(Vertical synchronization),簡(jiǎn)稱 V-Sync 來解決犀盟。
V-Sync
V-Sync 的主要作用就是保證只有在幀緩沖區(qū)中的圖像被渲染之后而晒,后備緩沖區(qū)中的內(nèi)容才可以被拷貝到幀緩沖區(qū)中,理想情況下的 V-Sync 會(huì)按這種方式工作:
每次 V-Sync 發(fā)生時(shí)阅畴,CPU 以及 GPU 都已經(jīng)完成了對(duì)圖像的處理以及繪制倡怎,顯示器可以直接拿到緩沖區(qū)中的幀。但是恶阴,如果 CPU 或者 GPU 的處理需要的時(shí)間較長,就會(huì)發(fā)生掉幀的問題:
在 V-Sync 信號(hào)發(fā)出時(shí)豹障,CPU 和 GPU 并沒有準(zhǔn)備好需要渲染的幀冯事,顯示器就會(huì)繼續(xù)使用當(dāng)前幀,這就加劇了屏幕的顯示問題血公,而每秒顯示的幀數(shù)會(huì)少于 60昵仅。
由于會(huì)發(fā)生很多次掉幀,在開啟了 V-Sync 后,40 ~ 50 FPS 的渲染頻率意味著顯示器輸出的畫面幀率會(huì)從 60 FPS 急劇下降到 30 FPS摔笤,原因在這里不會(huì)解釋够滑,讀者可以自行思考。
其實(shí)到這里關(guān)于屏幕渲染的內(nèi)容就已經(jīng)差不多結(jié)束了吕世,根據(jù) V-Sync 的原理彰触,優(yōu)化應(yīng)用性能、提高 App 的 FPS 就可以從兩個(gè)方面來入手命辖,優(yōu)化 CPU 以及 GPU 的處理時(shí)間况毅。
讀者也可以從 iOS 保持界面流暢的技巧這篇文章中了解更多的相關(guān)內(nèi)容。
性能調(diào)優(yōu)的策略
CPU 和 GPU 在每次 V-Sync 時(shí)間點(diǎn)到達(dá)之前都在干什么尔艇?如果尔许,我們知道了它們各自負(fù)責(zé)的工作,通過優(yōu)化代碼就可以提升性能终娃。
很多 CPU 的操作都會(huì)延遲 GPU 開始渲染的時(shí)間:
- 布局的計(jì)算 - 如果你的視圖層級(jí)太過于復(fù)雜味廊,或者視圖需要重復(fù)多次進(jìn)行布局,尤其是在使用 Auto Layout 進(jìn)行自動(dòng)布局時(shí)棠耕,對(duì)性能影響尤為嚴(yán)重余佛;
- 視圖的惰性加載 - 在 iOS 中只有當(dāng)視圖控制器的視圖顯示到屏幕時(shí)才會(huì)加載;
- 解壓圖片 - iOS 通常會(huì)在真正繪制時(shí)才會(huì)解碼圖片昧辽,對(duì)于一個(gè)較大的圖片衙熔,無論是直接或間接使用
UIImageView
或者繪制到 Core Graphics 中,都需要對(duì)圖片進(jìn)行解壓搅荞; - ...
寬泛的說红氯,大多數(shù)的 CALayer
的屬性都是由 GPU 來繪制的,比如圖片的圓角咕痛、變換痢甘、應(yīng)用紋理;但是過多的幾何結(jié)構(gòu)茉贡、重繪塞栅、離屏繪制(Offscrren)以及過大的圖片都會(huì)導(dǎo)致 GPU 的性能明顯降低。
上面的內(nèi)容出自 CPU vs GPU · iOS 核心動(dòng)畫高級(jí)技巧腔丧,你可以在上述文章中對(duì) CPU 和 GPU 到底各自做了什么有一個(gè)更深的了解放椰。
也就是說,如果我們解決了上述問題愉粤,就能加快應(yīng)用的渲染速度砾医,大大提升用戶體驗(yàn)。
AsyncDisplayKit
文章的前半部分已經(jīng)從屏幕的渲染原理講到了性能調(diào)優(yōu)的幾個(gè)策略衣厘;而 AsyncDisplayKit 就根據(jù)上述的策略幫助我們對(duì)應(yīng)用性能進(jìn)行優(yōu)化如蚜。
AsyncDisplayKit(以下簡(jiǎn)稱 ASDK)是由 Facebook 開源的一個(gè) iOS 框架压恒,能夠幫助最復(fù)雜的 UI 界面保持流暢和快速響應(yīng)。
ASDK 從開發(fā)到開源大約經(jīng)歷了一年多的時(shí)間错邦,它其實(shí)并不是一個(gè)簡(jiǎn)單的框架它是一個(gè)復(fù)雜的框架探赫,更像是對(duì) UIKit 的重新實(shí)現(xiàn),把整個(gè) UIKit 以及 CALayer 層封裝成一個(gè)一個(gè) Node
撬呢,將昂貴的渲染伦吠、圖片解碼、布局以及其它 UI 操作移出主線程倾芝,這樣主線程就可以對(duì)用戶的操作及時(shí)做出反應(yīng)讨勤。
很多分析 ASDK 的文章都會(huì)有這么一張圖介紹框架中的最基本概念:
在 ASDK 中最基本的單位就是 ASDisplayNode
,每一個(gè) node 都是對(duì) UIView
以及 CALayer
的抽象晨另。但是與 UIView
不同的是潭千,ASDisplayNode
是線程安全的,它可以在后臺(tái)線程中完成初始化以及配置工作借尿。
如果按照 60 FPS 的刷新頻率來計(jì)算刨晴,每一幀的渲染時(shí)間只有 16ms,在 16ms 的時(shí)間內(nèi)要完成對(duì) UIView
的創(chuàng)建路翻、布局狈癞、繪制以及渲染,CPU 和 GPU 面臨著巨大的壓力茂契。
但是從 A5 處理器之后蝶桶,多核的設(shè)備成為了主流,原有的將所有操作放入主線程的實(shí)踐已經(jīng)不能適應(yīng)復(fù)雜的 UI 界面掉冶,所以 ASDK 將耗時(shí)的 CPU 操作以及 GPU 渲染紋理(Texture)的過程全部放入后臺(tái)進(jìn)程真竖,使主線程能夠快速響應(yīng)用戶操作。
ASDK 通過獨(dú)特的渲染技巧厌小、代替 AutoLayout 的布局系統(tǒng)恢共、智能的預(yù)加載方式等模塊來實(shí)現(xiàn)對(duì) App 性能的優(yōu)化。
ASDK 的渲染過程
ASDK 中到底使用了哪些方法來對(duì)視圖進(jìn)行渲染呢璧亚?本文主要會(huì)從渲染的過程開始分析讨韭,了解 ASDK 底層如何提升界面的渲染性能。
在 ASDK 中的渲染圍繞 ASDisplayNode
進(jìn)行癣蟋,其過程總共有四條主線:
- 初始化
ASDisplayNode
對(duì)應(yīng)的UIView
或者CALayer
透硝; - 在當(dāng)前視圖進(jìn)入視圖層級(jí)時(shí)執(zhí)行
setNeedsDisplay
; -
display
方法執(zhí)行時(shí)疯搅,向后臺(tái)線程派發(fā)繪制事務(wù)濒生; - 注冊(cè)成為
RunLoop
觀察者,在每個(gè)RunLoop
結(jié)束時(shí)回調(diào)秉撇。
UIView 和 CALayer 的加載
當(dāng)我們運(yùn)行某一個(gè)使用 ASDK 的工程時(shí)甜攀,-[ASDisplayNode _loadViewOrLayerIsLayerBacked:]
總是 ASDK 中最先被調(diào)用的方法,而這個(gè)方法執(zhí)行的原因往往就是 ASDisplayNode
對(duì)應(yīng)的 UIView
和 CALayer
被引用了:
- (CALayer *)layer {
if (!_layer) {
ASDisplayNodeAssertMainThread();
if (!_flags.layerBacked) return self.view.layer;
[self _loadViewOrLayerIsLayerBacked:YES];
}
return _layer;
}
- (UIView *)view {
if (_flags.layerBacked) return nil;
if (!_view) {
ASDisplayNodeAssertMainThread();
[self _loadViewOrLayerIsLayerBacked:NO];
}
return _view;
}
這里涉及到一個(gè) ASDK 中比較重要的概念琐馆,如果 ASDisplayNode
是 layerBacked
的规阀,它不會(huì)渲染對(duì)應(yīng)的 UIView
以此來提升性能:
- (void)_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked {
if (isLayerBacked) {
_layer = [self _layerToLoad];
_layer.delegate = (id<CALayerDelegate>)self;
} else {
_view = [self _viewToLoad];
_view.asyncdisplaykit_node = self;
_layer = _view.layer;
}
_layer.asyncdisplaykit_node = self;
self.asyncLayer.asyncDelegate = self;
}
因?yàn)?UIView
和 CALayer
雖然都可以用于展示內(nèi)容,不過由于 UIView
可以用于處理用戶的交互瘦麸,所以如果不需要使用 UIView
的特性谁撼,直接使用 CALayer
進(jìn)行渲染,能夠節(jié)省大量的渲染時(shí)間滋饲。
如果你使用 Xcode 查看過視圖的層級(jí)厉碟,那么你應(yīng)該知道,
UIView
在 Debug View Hierarchy 中是有層級(jí)的屠缭;而CALayer
并沒有箍鼓,它門的顯示都在一個(gè)平面上。
上述方法中的 -[ASDisplayNode _layerToLoad]
以及 [ASDisplayNode _viewToLoad]
都只會(huì)根據(jù)當(dāng)前節(jié)點(diǎn)的 layerClass
或者 viewClass
初始化一個(gè)對(duì)象呵曹。
Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations 這篇文章比較了
UIView
和CALayer
的渲染時(shí)間款咖。
-[ASDisplayNode asyncLayer]
只是對(duì)當(dāng)前 node
持有的 layer
進(jìn)行封裝,確保會(huì)返回一個(gè) _ASDisplayLayer
的實(shí)例:
- (_ASDisplayLayer *)asyncLayer {
ASDN::MutexLocker l(_propertyLock);
return [_layer isKindOfClass:[_ASDisplayLayer class]] ? (_ASDisplayLayer *)_layer : nil;
}
最重要的是 -[ASDisplayNode _loadViewOrLayerIsLayerBacked:]
方法會(huì)將當(dāng)前節(jié)點(diǎn)設(shè)置為 asyncLayer
的代理奄喂,在后面會(huì)使用 ASDisplayNode
為 CALayer
渲染內(nèi)容铐殃。
視圖層級(jí)
在初始化工作完成之后,當(dāng) ASDisplayNode
第一次被加入到視圖的層級(jí)時(shí)跨新,-[_ASDisplayView willMoveToWindow:]
就會(huì)被調(diào)用富腊。
_ASDisplayView 和 _ASDisplayLayer
_ASDisplayView
和 _ASDisplayLayer
都是私有類,它們之間的對(duì)應(yīng)關(guān)系其實(shí)和 UIView
與 CALayer
完全相同域帐。
+ (Class)layerClass {
return [_ASDisplayLayer class];
}
_ASDisplayView
覆寫了很多跟視圖層級(jí)改變有關(guān)的方法:
-[_ASDisplayView willMoveToWindow:]
-[_ASDisplayView didMoveToWindow]
-[_ASDisplayView willMoveToSuperview:]
-[_ASDisplayView didMoveToSuperview]
它們用于在視圖的層級(jí)改變時(shí)赘被,通知對(duì)應(yīng) ASDisplayNode
作出相應(yīng)的反應(yīng),比如 -[_ASDisplayView willMoveToWindow:]
方法會(huì)在視圖被加入層級(jí)時(shí)調(diào)用:
- (void)willMoveToWindow:(UIWindow *)newWindow {
BOOL visible = (newWindow != nil);
if (visible && !_node.inHierarchy) {
[_node __enterHierarchy];
}
}
setNeedsDisplay
當(dāng)前視圖如果不在視圖層級(jí)中俯树,就會(huì)通過 _node
的實(shí)例方法 -[ASDisplayNode __enterHierarchy]
加入視圖層級(jí):
- (void)__enterHierarchy {
if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) {
_flags.isEnteringHierarchy = YES;
_flags.isInHierarchy = YES;
if (_flags.shouldRasterizeDescendants) {
[self _recursiveWillEnterHierarchy];
} else {
[self willEnterHierarchy];
}
_flags.isEnteringHierarchy = NO;
# 更新 layer 顯示的內(nèi)容
}
}
_flags
是ASDisplayNodeFlags
結(jié)構(gòu)體帘腹,用于標(biāo)記當(dāng)前ASDisplayNode
的一些 BOOL 值,比如许饿,異步顯示阳欲、柵格化子視圖等等,你不需要知道都有什么陋率,根據(jù)這些值的字面意思理解就已經(jīng)足夠了球化。
上述方法的前半部分只是對(duì) _flags
的標(biāo)記,如果需要將當(dāng)前視圖的子視圖柵格化瓦糟,也就是將它的全部子視圖與當(dāng)前視圖壓縮成一個(gè)圖層筒愚,就會(huì)向這些視圖遞歸地調(diào)用 -[ASDisplayNode willEnterHierarchy]
方法通知目前的狀態(tài):
- (void)_recursiveWillEnterHierarchy {
_flags.isEnteringHierarchy = YES;
[self willEnterHierarchy];
_flags.isEnteringHierarchy = NO;
for (ASDisplayNode *subnode in self.subnodes) {
[subnode _recursiveWillEnterHierarchy];
}
}
而 -[ASDisplayNode willEnterHierarchy]
會(huì)修改當(dāng)前節(jié)點(diǎn)的 interfaceState
到 ASInterfaceStateInHierarchy
,表示當(dāng)前節(jié)點(diǎn)不包含在 cell
或者其它菩浙,但是在 window
中巢掺。
- (void)willEnterHierarchy {
if (![self supportsRangeManagedInterfaceState]) {
self.interfaceState = ASInterfaceStateInHierarchy;
}
}
當(dāng)前結(jié)點(diǎn)需要被顯示在屏幕上時(shí)句伶,如果其內(nèi)容 contents
為空,就會(huì)調(diào)用 -[CALayer setNeedsDisplay]
方法將 CALayer
標(biāo)記為臟的陆淀,通知系統(tǒng)需要在下一個(gè)繪制循環(huán)中重繪視圖:
- (void)__enterHierarchy {
if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) {
# 標(biāo)記節(jié)點(diǎn)的 flag
if (self.contents == nil) {
CALayer *layer = self.layer;
[layer setNeedsDisplay];
if ([self _shouldHavePlaceholderLayer]) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self _setupPlaceholderLayerIfNeeded];
_placeholderLayer.opacity = 1.0;
[CATransaction commit];
[layer addSublayer:_placeholderLayer];
}
}
}
}
在將 CALayer
標(biāo)記為 dirty 之后考余,在繪制循環(huán)中就會(huì)執(zhí)行 -[CALayer display]
方法,對(duì)它要展示的內(nèi)容進(jìn)行繪制轧苫;如果當(dāng)前視圖需要一些占位圖楚堤,那么就會(huì)在這里的代碼中,為當(dāng)前 node
對(duì)應(yīng)的 layer
添加合適顏色的占位層含懊。
派發(fā)異步繪制事務(wù)
在上一節(jié)中調(diào)用 -[CALayer setNeedsDisplay]
方法將當(dāng)前節(jié)點(diǎn)標(biāo)記為 dirty 之后身冬,在下一個(gè)繪制循環(huán)時(shí)就會(huì)對(duì)所有需要重繪的 CALayer
執(zhí)行 -[CALayer display]
,這也是這一小節(jié)需要分析的方法的入口:
- (void)display {
[self _hackResetNeedsDisplay];
ASDisplayNodeAssertMainThread();
if (self.isDisplaySuspended) return;
[self display:self.displaysAsynchronously];
}
這一方法的調(diào)用棧比較復(fù)雜岔乔,在具體分析之前酥筝,筆者會(huì)先給出這個(gè)方法的調(diào)用棧,給讀者一個(gè)關(guān)于該方法實(shí)現(xiàn)的簡(jiǎn)要印象:
-[_ASDisplayLayer display]
-[_ASDisplayLayer display:] // 將繪制工作交給 ASDisplayNode 處理
-[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]
-[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:]
-[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]
-[CALayer(ASDisplayNodeAsyncTransactionContainer) asyncdisplaykit_parentTransactionContainer]
-[CALayer(ASDisplayNodeAsyncTransactionContainer) asyncdisplaykit_asyncTransaction]
-[_ASAsyncTransaction initWithCallbackQueue:completionBlock:]
-[_ASAsyncTransactionGroup addTransactionContainer:]
-[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]
ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block)
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
-[_ASDisplayLayer display]
在調(diào)用棧中其實(shí)會(huì)創(chuàng)建一個(gè) displayBlock
雏门,它其實(shí)是一個(gè)使用 Core Graphics 進(jìn)行圖像繪制的過程樱哼,整個(gè)繪制過程是通過事務(wù)的形式進(jìn)行管理的;而 displayBlock
會(huì)被 GCD 分發(fā)到后臺(tái)的并發(fā)進(jìn)程來處理剿配。
調(diào)用棧中的第二個(gè)方法 -[_ASDisplayLayer display]
會(huì)將異步繪制的工作交給自己的 asyncDelegate
搅幅,也就是第一部分中設(shè)置的 ASDisplayNode
:
- (void)display:(BOOL)asynchronously {
[_asyncDelegate displayAsyncLayer:self asynchronously:asynchronously];
}
ASDisplayNode(AsyncDisplay)
這里省略了一部分 -[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]
方法的實(shí)現(xiàn):
- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously {
ASDisplayNodeAssertMainThread();
...
asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO];
if (!displayBlock) return;
asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
ASDisplayNodeCAssertMainThread();
if (!canceled && !isCancelledBlock()) {
UIImage *image = (UIImage *)value;
_layer.contentsScale = self.contentsScale;
_layer.contents = (id)image.CGImage;
}
};
if (asynchronously) {
CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ? : _layer;
_ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
[transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
} else {
UIImage *contents = (UIImage *)displayBlock();
completionBlock(contents, NO);
}
}
省略后的代碼脈絡(luò)非常清晰,-[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:]
返回一個(gè)用于 displayBlock
呼胚,然后構(gòu)造一個(gè) completionBlock
茄唐,在繪制結(jié)束時(shí)執(zhí)行,在主線程中設(shè)置當(dāng)前 layer
的內(nèi)容蝇更。
如果當(dāng)前的渲染是異步的沪编,就會(huì)將 displayBlock
包裝成一個(gè)事務(wù),添加到隊(duì)列中執(zhí)行年扩,否則就會(huì)同步執(zhí)行當(dāng)前的 block蚁廓,并執(zhí)行 completionBlock
回調(diào),通知 layer
更新顯示的內(nèi)容厨幻。
同步顯示的部分到這里已經(jīng)很清楚了相嵌,我們更關(guān)心的其實(shí)還是異步繪制的部分,因?yàn)檫@部分才是 ASDK 提升效率的關(guān)鍵况脆;而這就要從獲取 displayBlock
的方法開始了解了饭宾。
displayBlock 的構(gòu)建
displayBlock
的創(chuàng)建一般分為三種不同的方式:
- 將當(dāng)前視圖的子視圖壓縮成一層繪制在當(dāng)前頁面上
- 使用
- displayWithParameters:isCancelled:
返回一個(gè)UIImage
,對(duì)圖像節(jié)點(diǎn)ASImageNode
進(jìn)行繪制 - 使用
- drawRect:withParameters:isCancelled:isRasterizing:
在 CG 上下文中繪制文字節(jié)點(diǎn)ASTextNode
這三種方式都通過 ASDK 來優(yōu)化視圖的渲染速度格了,這些操作最后都會(huì)扔到后臺(tái)的并發(fā)線程中進(jìn)行處理看铆。
下面三個(gè)部分的代碼經(jīng)過了刪減,省略了包括取消繪制盛末、通知代理弹惦、控制并發(fā)數(shù)量以及用于調(diào)試的代碼否淤。
柵格化子視圖
如果當(dāng)前的視圖需要柵格化子視圖,就會(huì)進(jìn)入啟用下面的構(gòu)造方式創(chuàng)建一個(gè) block棠隐,它會(huì)遞歸地將子視圖繪制在父視圖上:
- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing {
asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
ASDisplayNodeFlags flags = _flags;
if (!rasterizing && self.shouldRasterizeDescendants) {
NSMutableArray *displayBlocks = [NSMutableArray array];
[self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks];
CGFloat contentsScaleForDisplay = self.contentsScaleForDisplay;
BOOL opaque = self.opaque && CGColorGetAlpha(self.backgroundColor.CGColor) == 1.0f;
displayBlock = ^id{
UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
for (dispatch_block_t block in displayBlocks) {
block();
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
};
} else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) {
#:繪制 UIImage
} else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) {
#:提供 context叹括,使用 CG 繪圖
}
return [displayBlock copy];
}
在壓縮視圖層級(jí)的過程中就會(huì)調(diào)用 -[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]
方法,獲取子視圖的所有 displayBlock
宵荒,在得到 UIGraphicsBeginImageContextWithOptions
需要的參數(shù)之后,創(chuàng)建一個(gè)新的 context净嘀,執(zhí)行了所有的 displayBlock
將子視圖的繪制到當(dāng)前圖層之后报咳,使用 UIGraphicsGetImageFromCurrentImageContext
取出圖層的內(nèi)容并返回。
-[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]
的實(shí)現(xiàn)還是有些繁瑣的挖藏,它主要的功能就是使用 Core Graphics 進(jìn)行繪圖暑刃,將背景顏色、仿射變換膜眠、位置大小以及圓角等參數(shù)繪制到當(dāng)前的上下文中岩臣,而且這個(gè)過程是遞歸的,直到不存在或者不需要繪制子節(jié)點(diǎn)為止宵膨。
繪制圖片
displayBlock
的第二種繪制策略更多地適用于圖片節(jié)點(diǎn) ASImageNode
的繪制:
- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing {
asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
ASDisplayNodeFlags flags = _flags;
if (!rasterizing && self.shouldRasterizeDescendants) {
#:柵格化
} else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) {
id drawParameters = [self drawParameters];
displayBlock = ^id{
UIImage *result = nil;
if (flags.implementsInstanceImageDisplay) {
result = [self displayWithParameters:drawParameters isCancelled:isCancelledBlock];
} else {
result = [[self class] displayWithParameters:drawParameters isCancelled:isCancelledBlock];
}
return result;
};
} else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) {
#:提供 context架谎,使用 CG 繪圖
}
return [displayBlock copy];
}
通過 - displayWithParameters:isCancelled:
的執(zhí)行返回一個(gè)圖片,不過這里的繪制也離不開 Core Graphics 的一些 C 函數(shù)辟躏,你會(huì)在 -[ASImageNode displayWithParameters:isCancelled:]
中看到對(duì)于 CG 的運(yùn)用谷扣,它會(huì)使用 drawParameters
來修改并繪制自己持有的 image
對(duì)象。
使用 CG 繪圖
文字的繪制一般都會(huì)在 - drawRect:withParameters:isCancelled:isRasterizing:
進(jìn)行捎琐,這個(gè)方法只是提供了一個(gè)合適的用于繪制的上下文会涎,該方法不止可以繪制文字,只是在這里繪制文字比較常見:
- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing {
asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
ASDisplayNodeFlags flags = _flags;
if (!rasterizing && self.shouldRasterizeDescendants) {
#:柵格化
} else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) {
#:繪制 UIImage
} else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) {
if (!rasterizing) {
UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
}
if (flags.implementsInstanceDrawRect) {
[self drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
} else {
[[self class] drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
}
UIImage *image = nil;
if (!rasterizing) {
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
return image;
};
}
return [displayBlock copy];
}
上述代碼跟第一部分比較像瑞凑,區(qū)別是這里不會(huì)柵格化子視圖末秃;代碼根據(jù)情況會(huì)決定是否重新開一個(gè)新的上下文,然后通過 - drawRect:withParameters:isCancelled:isRasterizing:
方法實(shí)現(xiàn)繪制籽御。
管理繪制事務(wù)
ASDK 提供了一個(gè)私有的管理事務(wù)的機(jī)制练慕,由三部分組成 _ASAsyncTransactionGroup
、_ASAsyncTransactionContainer
以及 _ASAsyncTransaction
技掏,這三者各自都有不同的功能:
-
_ASAsyncTransactionGroup
會(huì)在初始化時(shí)贺待,向 Runloop 中注冊(cè)一個(gè)回調(diào),在每次 Runloop 結(jié)束時(shí)零截,執(zhí)行回調(diào)來提交displayBlock
執(zhí)行的結(jié)果 -
_ASAsyncTransactionContainer
為當(dāng)前CALayer
提供了用于保存事務(wù)的容器麸塞,并提供了獲取新的_ASAsyncTransaction
實(shí)例的便利方法 -
_ASAsyncTransaction
將異步操作封裝成了輕量級(jí)的事務(wù)對(duì)象,使用 C++ 代碼對(duì) GCD 進(jìn)行了封裝
從上面的小節(jié)中涧衙,我們已經(jīng)獲取到了用于繪制的 displayBlock
哪工,然后就需要將 block 添加到繪制事務(wù)中:
- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously {
...
if (asynchronously) {
CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ? : _layer;
_ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
[transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
} else {
...
}
}
前兩行代碼是獲取 _ASAsyncTransaction
實(shí)例的過程奥此,這個(gè)實(shí)例會(huì)包含在一個(gè) layer
的哈希表中,最后調(diào)用的實(shí)例方法 -[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]
會(huì)把用于繪制的 displayBlock
添加到后臺(tái)并行隊(duì)列中:
+ (dispatch_queue_t)displayQueue {
static dispatch_queue_t displayQueue = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
displayQueue = dispatch_queue_create("org.AsyncDisplayKit.ASDisplayLayer.displayQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_set_target_queue(displayQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
});
return displayQueue;
}
這個(gè)隊(duì)列是一個(gè)并行隊(duì)列雁比,并且優(yōu)先級(jí)是 DISPATCH_QUEUE_PRIORITY_HIGH
稚虎,確保 UI 的渲染會(huì)在其它異步操作執(zhí)行之前進(jìn)行,而 -[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]
中會(huì)初始化 ASDisplayNodeAsyncTransactionOperation
的實(shí)例偎捎,然后傳入 completionBlock
蠢终,在繪制結(jié)束時(shí)回調(diào):
- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block priority:(NSInteger)priority queue:(dispatch_queue_t)queue completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion {
ASDisplayNodeAssertMainThread();
[self _ensureTransactionData];
ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion];
[_operations addObject:operation];
_group->schedule(priority, queue, ^{
@autoreleasepool {
operation.value = block();
}
});
}
schedule
方法是一個(gè) C++ 方法,它會(huì)向 ASAsyncTransactionQueue::Group
中派發(fā)一個(gè) block茴她,這個(gè) block 中會(huì)執(zhí)行 displayBlock
寻拂,然后將結(jié)果傳給 operation.value
:
void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) {
ASAsyncTransactionQueue &q = _queue;
ASDN::MutexLocker locker(q._mutex);
DispatchEntry &entry = q._entries[queue];
Operation operation;
operation._block = block;
operation._group = this;
operation._priority = priority;
entry.pushOperation(operation);
++_pendingOperations;
NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2;
if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode])
--maxThreads;
if (entry._threadCount < maxThreads) {
bool respectPriority = entry._threadCount > 0;
++entry._threadCount;
dispatch_async(queue, ^{
while (!entry._operationQueue.empty()) {
Operation operation = entry.popNextOperation(respectPriority);
{
if (operation._block) {
operation._block();
}
operation._group->leave();
operation._block = nil;
}
}
--entry._threadCount;
if (entry._threadCount == 0) {
q._entries.erase(queue);
}
});
}
}
ASAsyncTransactionQueue::GroupImpl
其實(shí)現(xiàn)其實(shí)就是對(duì) GCD 的封裝,同時(shí)添加一些最大并發(fā)數(shù)丈牢、線程鎖的功能祭钉。通過 dispatch_async
將 block 分發(fā)到 queue
中,立刻執(zhí)行 block己沛,將數(shù)據(jù)傳回 ASDisplayNodeAsyncTransactionOperation
實(shí)例慌核。
回調(diào)
在 _ASAsyncTransactionGroup
調(diào)用 mainTransactionGroup
類方法獲取單例時(shí),會(huì)通過 +[_ASAsyncTransactionGroup registerTransactionGroupAsMainRunloopObserver]
向 Runloop 中注冊(cè)回調(diào):
+ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup {
static CFRunLoopObserverRef observer;
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFOptionFlags activities = (kCFRunLoopBeforeWaiting | kCFRunLoopExit);
CFRunLoopObserverContext context = {0, (__bridge void *)transactionGroup, &CFRetain, &CFRelease, NULL};
observer = CFRunLoopObserverCreate(NULL, activities, YES, INT_MAX, &_transactionGroupRunLoopObserverCallback, &context);
CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
}
上述代碼會(huì)在即將退出 Runloop 或者 Runloop 開始休眠時(shí)執(zhí)行回調(diào) _transactionGroupRunLoopObserverCallback
,而這個(gè)回調(diào)方法就是這一條主線的入口:
static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
ASDisplayNodeCAssertMainThread();
_ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info;
[group commit];
}
上一節(jié)中只是會(huì)將繪制代碼提交到后臺(tái)的并發(fā)進(jìn)程中,而這里才會(huì)將結(jié)果提交救湖,也就是在每次 Runloop 循環(huán)結(jié)束時(shí)開始繪制內(nèi)容振劳,而 -[_operationCompletionBlock commit]
方法的調(diào)用棧能夠幫助我們理解內(nèi)容是如何提交的,又是如何傳回 node
持有的 layer
的:
-[_ASAsyncTransactionGroup commit]
-[_ASAsyncTransaction commit]
ASAsyncTransactionQueue::GroupImpl::notify(dispatch_queue_t, dispatch_block_t)
_notifyList.push_back(GroupNotify)
-[_ASAsyncTransactionGroup commit]
方法的調(diào)用完成了對(duì)繪制事務(wù)的提交,而在 -[_ASAsyncTransaction commit]
中會(huì)調(diào)用 notify
方法,在上一節(jié)中的 displayBlock
執(zhí)行結(jié)束后調(diào)用這里傳入的 block 執(zhí)行 -[_ASAsyncTransaction completeTransaction]
方法:
- (void)commit {
ASDisplayNodeAssertMainThread();
__atomic_store_n(&_state, ASAsyncTransactionStateCommitted, __ATOMIC_SEQ_CST);
_group->notify(_callbackQueue, ^{
ASDisplayNodeAssertMainThread();
[self completeTransaction];
});
}
我們按照時(shí)間順序來分析在上面的 block 執(zhí)行之前,方法是如何調(diào)用的钾怔,以及 block 是如何被執(zhí)行的;這就不得不回到派發(fā)繪制事務(wù)的部分了蒙挑,在 ASAsyncTransactionQueue::GroupImpl::schedule
方法中宗侦,使用了 dispatch_async
將派發(fā) block:
void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) {
...
if (entry._threadCount < maxThreads) {
...
dispatch_async(queue, ^{
...
while (!entry._operationQueue.empty()) {
Operation operation = entry.popNextOperation(respectPriority);
{
ASDN::MutexUnlocker unlock(q._mutex);
if (operation._block) {
operation._block();
}
operation._group->leave();
operation._block = nil;
}
}
...
});
}
}
在 displayBlock
執(zhí)行之后,會(huì)調(diào)用的 group
的 leave
方法:
void ASAsyncTransactionQueue::GroupImpl::leave() {
if (_pendingOperations == 0) {
std::list<GroupNotify> notifyList;
_notifyList.swap(notifyList);
for (GroupNotify & notify : notifyList) {
dispatch_async(notify._queue, notify._block);
}
}
}
這里終于執(zhí)行了在 - commit
中加入的 block忆蚀,也就是 -[_ASAsyncTransaction completeTransaction]
方法:
- (void)completeTransaction {
if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateComplete) {
BOOL isCanceled = (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateCanceled);
for (ASDisplayNodeAsyncTransactionOperation *operation in _operations) {
[operation callAndReleaseCompletionBlock:isCanceled];
}
__atomic_store_n(&_state, ASAsyncTransactionStateComplete, __ATOMIC_SEQ_CST);
}
}
最后的最后矾利,-[ASDisplayNodeAsyncTransactionOperation callAndReleaseCompletionBlock:]
方法執(zhí)行了回調(diào)將 displayBlock
執(zhí)行的結(jié)果傳回了 CALayer:
- (void)callAndReleaseCompletionBlock:(BOOL)canceled; {
if (_operationCompletionBlock) {
_operationCompletionBlock(self.value, canceled);
self.operationCompletionBlock = nil;
}
}
也就是在 -[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]
方法中構(gòu)建的 completionBlock
:
asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
ASDisplayNodeCAssertMainThread();
if (!canceled && !isCancelledBlock()) {
UIImage *image = (UIImage *)value;
BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
if (stretchable) {
ASDisplayNodeSetupLayerContentsWithResizableImage(_layer, image);
} else {
_layer.contentsScale = self.contentsScale;
_layer.contents = (id)image.CGImage;
}
[self didDisplayAsyncLayer:self.asyncLayer];
}
};
這一部分進(jìn)行的大量的數(shù)據(jù)傳遞都是通過 block 進(jìn)行的,從 Runloop 中對(duì)事務(wù)的提交馋袜,以及通過 notify
方法加入的 block男旗,都是為了最后將繪制的結(jié)果傳回 CALayer
對(duì)象,而到這里可以說整個(gè) ASDK 對(duì)于視圖內(nèi)容的繪制過程就結(jié)束了欣鳖。
總結(jié)
ASDK 對(duì)于繪制過程的優(yōu)化有三部分:分別是柵格化子視圖察皇、繪制圖像以及繪制文字。
它攔截了視圖加入層級(jí)時(shí)發(fā)出的通知 - willMoveToWindow:
方法,然后手動(dòng)調(diào)用 - setNeedsDisplay
什荣,強(qiáng)制所有的 CALayer
執(zhí)行 - display
更新內(nèi)容矾缓;
然后將上面的操作全部拋入了后臺(tái)的并發(fā)線程中,并在 Runloop 中注冊(cè)回調(diào)稻爬,在每次 Runloop 結(jié)束時(shí)嗜闻,對(duì)已經(jīng)完成的事務(wù)進(jìn)行 - commit
,以圖片的形式直接傳回對(duì)應(yīng)的 layer.content
中桅锄,完成對(duì)內(nèi)容的更新琉雳。
從它的實(shí)現(xiàn)來看,確實(shí)從主線程移除了很多昂貴的 CPU 以及 GPU 操作友瘤,有效地加快了視圖的繪制和渲染翠肘,保證了主線程的流暢執(zhí)行。
References
- How VSync works, and why people loathe it
- 腦洞大開:為啥幀率達(dá)到 60 fps 就流暢商佑?
- iOS 保持界面流暢的技巧
- CADiplayLink Class Reference - Developer- Apple
- CPU vs GPU · iOS 核心動(dòng)畫高級(jí)技巧
- 理解 UIView 的繪制
- Introduce to AsyncDisplayKit
- AsyncDisplayKit Tutorial: Achieving 60 FPS scrolling
- Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations
- 深入理解 RunLoop
其它
Github Repo:iOS-Source-Code-Analyze
Follow: Draveness · Github