這一系列的文章會從幾個方面對 ASDK 在效能調優方面策略的實現進行分析,幫助讀者理解 ASDK 如何做到使複雜的 UI 介面達到 60 FPS 的重新整理頻率的;本篇文章會從檢視的渲染層面講解 ASDK 對於渲染過程的優化並對 ASDK 進行概述。
在客戶端或者前端開發中,對於效能的優化,尤其是 UI,往往都不是最先考慮的問題。
因為在大多數場景下,使用更加複雜的高效能程式碼替代可用的程式碼經常會導致程式碼的可維護性下降,所以更需要我們開發者對優化的時間點以及原因有一個比較清楚的認識,避免過度優化帶來的問題。
對 iOS 開發比較熟悉的開發者都知道,iOS 中的效能問題大多是阻塞主執行緒導致使用者的互動反饋出現可以感知的延遲。
詳細說起來,大體有三種原因:
- UI 渲染需要時間較長,無法按時提交結果;
- 一些需要密集計算的處理放在了主執行緒中執行,導致主執行緒被阻塞,無法渲染 UI 介面;
- 網路請求由於網路狀態的問題響應較慢,UI 層由於沒有模型返回無法渲染。
上面的這些問題都會影響應用的效能,最常見的表現就是 UITableView
在滑動時沒有達到 60 FPS,使用者能感受到明顯的卡頓。
螢幕的渲染
相信點開這篇文章的大多數開發者都知道 FPS 是什麼,那麼如果才能優化我們的 App 使其達到 60 FPS 呢?在具體瞭解方法之前,我們先退一步,提出另一個問題,螢幕是如何渲染的?
對於第一個問題,可能需要幾篇文章來回答,希望整個系列的文章能給你一個滿意的答案。3
CRT 和 LCD
螢幕的渲染可能要從 CRT(Cathode ray tube) 顯示器和 LCD(Liquid-crystal display) 顯示器講起。
CRT 顯示器是比較古老的技術,它使用陰極電子槍發射電子,在陰極高壓的作用下,電子由電子槍射向熒光屏,使熒光粉發光,將影象顯示在螢幕上,這也就是用磁鐵靠近一些老式電視機的螢幕會讓它們變色的原因。
而 FPS 就是 CRT 顯示器的重新整理頻率,電子槍每秒會對顯示器上內容進行 60 - 100 次的重新整理,哪怕在我們看來沒有任何改變。
但是 LCD 的原理與 CRT 非常不同,LCD 的成像原理跟光學有關:
- 在不加電壓下,光線會沿著液晶分子的間隙前進旋轉 90°,所以光可以通過;
- 在加入電壓之後,光沿著液晶分子的間隙直線前進,被濾光板擋住。
如果你可以翻牆,相信下面的視訊會更好得幫助你理解 LCD 的工作原理:
LCD 的成像原理雖然與 CRT 截然不同,每一個畫素的顏色可以在需要改變時才去改變電壓,也就是不需要重新整理頻率,但是由於一些歷史原因,LCD 仍然需要按照一定的重新整理頻率向 GPU 獲取新的影象用於顯示。
螢幕撕裂
但是顯示器只是用於將影象顯示在螢幕上,誰又是影象的提供者呢?影象都是我們經常說的 GPU 提供的。
而這導致了另一個問題,由於 GPU 生成影象的頻率與顯示器重新整理的頻率是不相關的,那麼在顯示器重新整理時,GPU 沒有準備好需要顯示的影象怎麼辦;或者 GPU 的渲染速度過快,顯示器來不及重新整理,GPU 就已經開始渲染下一幀影象又該如何處理?
如果解決不了這兩個問題,就會出現上圖中的螢幕撕裂(Screen Tearing)現象,螢幕中一部分顯示的是上一幀的內容,另一部分顯示的是下一幀的內容。
我們用兩個例子來說明可能出現螢幕撕裂的兩種情況:
- 如果顯示器的重新整理頻率為 75 Hz,GPU 的渲染速度為 100 Hz,那麼在兩次螢幕重新整理的間隔中,GPU 會渲染 4/3 個幀,後面的 1/3 幀會覆蓋已經渲染好的幀棧,最終會導致螢幕在 1/3 或者 2/3 的位置出現螢幕撕裂效果;
- 那麼 GPU 的渲染速度小於顯示器呢,比如說 50 Hz,那麼在兩次螢幕重新整理的間隔中,GPU 只會渲染 2/3 幀,剩下的 1/3 會來自上一幀,與上面的結果完全相同,在同樣的位置出現撕裂效果。
到這裡,有人會說,如果顯示器的重新整理頻率與 GPU 的渲染速度完全相同,應該就會解決螢幕撕裂的問題了吧?其實並不是。顯示器從 GPU 拷貝幀的過程依然需要消耗一定的時間,如果螢幕在拷貝影象時重新整理,仍然會導致螢幕撕裂問題。
引入多個緩衝區可以有效地緩解螢幕撕裂,也就是同時使用一個幀緩衝區(frame buffer)和多個後備緩衝區(back buffer);在每次顯示器請求內容時,都會從幀緩衝區中取出影象然後渲染。
雖然緩衝區可以減緩這些問題,但是卻不能解決;如果後備緩衝區繪製完成,而幀緩衝區的影象沒有被渲染,後備緩衝區中的影象就會覆蓋幀緩衝區,仍然會導致螢幕撕裂。
解決這個問題需要另一個機制的幫助,也就是垂直同步(Vertical synchronization),簡稱 V-Sync 來解決。
V-Sync
V-Sync 的主要作用就是保證只有在幀緩衝區中的影象被渲染之後,後備緩衝區中的內容才可以被拷貝到幀緩衝區中,理想情況下的 V-Sync 會按這種方式工作:
每次 V-Sync 發生時,CPU 以及 GPU 都已經完成了對影象的處理以及繪製,顯示器可以直接拿到緩衝區中的幀。但是,如果 CPU 或者 GPU 的處理需要的時間較長,就會發生掉幀的問題:
在 V-Sync 訊號發出時,CPU 和 GPU 並沒有準備好需要渲染的幀,顯示器就會繼續使用當前幀,這就加劇了螢幕的顯示問題,而每秒顯示的幀數會少於 60。
由於會發生很多次掉幀,在開啟了 V-Sync 後,40 ~ 50 FPS 的渲染頻率意味著顯示器輸出的畫面幀率會從 60 FPS 急劇下降到 30 FPS,原因在這裡不會解釋,讀者可以自行思考。
其實到這裡關於螢幕渲染的內容就已經差不多結束了,根據 V-Sync 的原理,優化應用效能、提高 App 的 FPS 就可以從兩個方面來入手,優化 CPU 以及 GPU 的處理時間。
讀者也可以從 iOS 保持介面流暢的技巧這篇文章中瞭解更多的相關內容。
效能調優的策略
CPU 和 GPU 在每次 V-Sync 時間點到達之前都在幹什麼?如果,我們知道了它們各自負責的工作,通過優化程式碼就可以提升效能。
很多 CPU 的操作都會延遲 GPU 開始渲染的時間:
- 佈局的計算 - 如果你的檢視層級太過於複雜,或者檢視需要重複多次進行佈局,尤其是在使用 Auto Layout 進行自動佈局時,對效能影響尤為嚴重;
- 檢視的惰性載入 - 在 iOS 中只有當檢視控制器的檢視顯示到螢幕時才會載入;
- 解壓圖片 - iOS 通常會在真正繪製時才會解碼圖片,對於一個較大的圖片,無論是直接或間接使用
UIImageView
或者繪製到 Core Graphics 中,都需要對圖片進行解壓; - ...
寬泛的說,大多數的 CALayer
的屬性都是由 GPU 來繪製的,比如圖片的圓角、變換、應用紋理;但是過多的幾何結構、重繪、離屏繪製(Offscrren)以及過大的圖片都會導致 GPU 的效能明顯降低。
上面的內容出自 CPU vs GPU · iOS 核心動畫高階技巧,你可以在上述文章中對 CPU 和 GPU 到底各自做了什麼有一個更深的瞭解。
也就是說,如果我們解決了上述問題,就能加快應用的渲染速度,大大提升使用者體驗。
AsyncDisplayKit
文章的前半部分已經從螢幕的渲染原理講到了效能調優的幾個策略;而 AsyncDisplayKit 就根據上述的策略幫助我們對應用效能進行優化。
AsyncDisplayKit(以下簡稱 ASDK)是由 Facebook 開源的一個 iOS 框架,能夠幫助最複雜的 UI 介面保持流暢和快速響應。
ASDK 從開發到開源大約經歷了一年多的時間,它其實並不是一個簡單的框架它是一個複雜的框架,更像是對 UIKit 的重新實現,把整個 UIKit 以及 CALayer 層封裝成一個一個 Node
,將昂貴的渲染、圖片解碼、佈局以及其它 UI 操作移出主執行緒,這樣主執行緒就可以對使用者的操作及時做出反應。
很多分析 ASDK 的文章都會有這麼一張圖介紹框架中的最基本概念:
在 ASDK 中最基本的單位就是 ASDisplayNode
,每一個 node 都是對 UIView
以及 CALayer
的抽象。但是與 UIView
不同的是,ASDisplayNode
是執行緒安全的,它可以在後臺執行緒中完成初始化以及配置工作。
如果按照 60 FPS 的重新整理頻率來計算,每一幀的渲染時間只有 16ms,在 16ms 的時間內要完成對 UIView
的建立、佈局、繪製以及渲染,CPU 和 GPU 面臨著巨大的壓力。
但是從 A5 處理器之後,多核的裝置成為了主流,原有的將所有操作放入主執行緒的實踐已經不能適應複雜的 UI 介面,所以 ASDK 將耗時的 CPU 操作以及 GPU 渲染紋理(Texture)的過程全部放入後臺程式,使主執行緒能夠快速響應使用者操作。
ASDK 通過獨特的渲染技巧、代替 AutoLayout 的佈局系統、智慧的預載入方式等模組來實現對 App 效能的優化。
ASDK 的渲染過程
ASDK 中到底使用了哪些方法來對檢視進行渲染呢?本文主要會從渲染的過程開始分析,瞭解 ASDK 底層如何提升介面的渲染效能。
在 ASDK 中的渲染圍繞 ASDisplayNode
進行,其過程總共有四條主線:
- 初始化
ASDisplayNode
對應的UIView
或者CALayer
; - 在當前檢視進入檢視層級時執行
setNeedsDisplay
; display
方法執行時,向後臺執行緒派發繪製事務;- 註冊成為
RunLoop
觀察者,在每個RunLoop
結束時回撥。
UIView 和 CALayer 的載入
當我們執行某一個使用 ASDK 的工程時,-[ASDisplayNode _loadViewOrLayerIsLayerBacked:]
總是 ASDK 中最先被呼叫的方法,而這個方法執行的原因往往就是 ASDisplayNode
對應的 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;
}複製程式碼
這裡涉及到一個 ASDK 中比較重要的概念,如果 ASDisplayNode
是 layerBacked
的,它不會渲染對應的 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;
}複製程式碼
因為 UIView
和 CALayer
雖然都可以用於展示內容,不過由於 UIView
可以用於處理使用者的互動,所以如果不需要使用 UIView
的特性,直接使用 CALayer
進行渲染,能夠節省大量的渲染時間。
如果你使用 Xcode 檢視過檢視的層級,那麼你應該知道,
UIView
在 Debug View Hierarchy 中是有層級的;而CALayer
並沒有,它門的顯示都在一個平面上。
上述方法中的 -[ASDisplayNode _layerToLoad]
以及 [ASDisplayNode _viewToLoad]
都只會根據當前節點的 layerClass
或者 viewClass
初始化一個物件。
Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations 這篇文章比較了
UIView
和CALayer
的渲染時間。
-[ASDisplayNode asyncLayer]
只是對當前 node
持有的 layer
進行封裝,確保會返回一個 _ASDisplayLayer
的例項:
- (_ASDisplayLayer *)asyncLayer {
ASDN::MutexLocker l(_propertyLock);
return [_layer isKindOfClass:[_ASDisplayLayer class]] ? (_ASDisplayLayer *)_layer : nil;
}複製程式碼
最重要的是 -[ASDisplayNode _loadViewOrLayerIsLayerBacked:]
方法會將當前節點設定為 asyncLayer
的代理,在後面會使用 ASDisplayNode
為 CALayer
渲染內容。
檢視層級
在初始化工作完成之後,當 ASDisplayNode
第一次被加入到檢視的層級時,-[_ASDisplayView willMoveToWindow:]
就會被呼叫。
_ASDisplayView 和 _ASDisplayLayer
_ASDisplayView
和 _ASDisplayLayer
都是私有類,它們之間的對應關係其實和 UIView
與 CALayer
完全相同。
+ (Class)layerClass {
return [_ASDisplayLayer class];
}複製程式碼
_ASDisplayView
覆寫了很多跟檢視層級改變有關的方法:
-[_ASDisplayView willMoveToWindow:]
-[_ASDisplayView didMoveToWindow]
-[_ASDisplayView willMoveToSuperview:]
-[_ASDisplayView didMoveToSuperview]
它們用於在檢視的層級改變時,通知對應 ASDisplayNode
作出相應的反應,比如 -[_ASDisplayView willMoveToWindow:]
方法會在檢視被加入層級時呼叫:
- (void)willMoveToWindow:(UIWindow *)newWindow {
BOOL visible = (newWindow != nil);
if (visible && !_node.inHierarchy) {
[_node __enterHierarchy];
}
}複製程式碼
setNeedsDisplay
當前檢視如果不在檢視層級中,就會通過 _node
的例項方法 -[ASDisplayNode __enterHierarchy]
加入檢視層級:
- (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 顯示的內容
}
}複製程式碼
_flags
是ASDisplayNodeFlags
結構體,用於標記當前ASDisplayNode
的一些 BOOL 值,比如,非同步顯示、柵格化子檢視等等,你不需要知道都有什麼,根據這些值的字面意思理解就已經足夠了。
上述方法的前半部分只是對 _flags
的標記,如果需要將當前檢視的子檢視柵格化,也就是將它的全部子檢視與當前檢視壓縮成一個圖層,就會向這些檢視遞迴地呼叫 -[ASDisplayNode willEnterHierarchy]
方法通知目前的狀態:
- (void)_recursiveWillEnterHierarchy {
_flags.isEnteringHierarchy = YES;
[self willEnterHierarchy];
_flags.isEnteringHierarchy = NO;
for (ASDisplayNode *subnode in self.subnodes) {
[subnode _recursiveWillEnterHierarchy];
}
}複製程式碼
而 -[ASDisplayNode willEnterHierarchy]
會修改當前節點的 interfaceState
到 ASInterfaceStateInHierarchy
,表示當前節點不包含在 cell
或者其它,但是在 window
中。
- (void)willEnterHierarchy {
if (![self supportsRangeManagedInterfaceState]) {
self.interfaceState = ASInterfaceStateInHierarchy;
}
}複製程式碼
當前結點需要被顯示在螢幕上時,如果其內容 contents
為空,就會呼叫 -[CALayer setNeedsDisplay]
方法將 CALayer
標記為髒的,通知系統需要在下一個繪製迴圈中重繪檢視:
- (void)__enterHierarchy {
if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) {
# 標記節點的 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
標記為 dirty 之後,在繪製迴圈中就會執行 -[CALayer display]
方法,對它要展示的內容進行繪製;如果當前檢視需要一些佔點陣圖,那麼就會在這裡的程式碼中,為當前 node
對應的 layer
新增合適顏色的佔位層。
派發非同步繪製事務
在上一節中呼叫 -[CALayer setNeedsDisplay]
方法將當前節點標記為 dirty 之後,在下一個繪製迴圈時就會對所有需要重繪的 CALayer
執行 -[CALayer display]
,這也是這一小節需要分析的方法的入口:
- (void)display {
[self _hackResetNeedsDisplay];
ASDisplayNodeAssertMainThread();
if (self.isDisplaySuspended) return;
[self display:self.displaysAsynchronously];
}複製程式碼
這一方法的呼叫棧比較複雜,在具體分析之前,筆者會先給出這個方法的呼叫棧,給讀者一個關於該方法實現的簡要印象:
-[_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]
在呼叫棧中其實會建立一個 displayBlock
,它其實是一個使用 Core Graphics 進行影象繪製的過程,整個繪製過程是通過事務的形式進行管理的;而 displayBlock
會被 GCD 分發到後臺的併發程式來處理。
呼叫棧中的第二個方法 -[_ASDisplayLayer display]
會將非同步繪製的工作交給自己的 asyncDelegate
,也就是第一部分中設定的 ASDisplayNode
:
- (void)display:(BOOL)asynchronously {
[_asyncDelegate displayAsyncLayer:self asynchronously:asynchronously];
}複製程式碼
ASDisplayNode(AsyncDisplay)
這裡省略了一部分 -[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]
方法的實現:
- (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);
}
}複製程式碼
省略後的程式碼脈絡非常清晰,-[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:]
返回一個用於 displayBlock
,然後構造一個 completionBlock
,在繪製結束時執行,在主執行緒中設定當前 layer
的內容。
如果當前的渲染是非同步的,就會將 displayBlock
包裝成一個事務,新增到佇列中執行,否則就會同步執行當前的 block,並執行 completionBlock
回撥,通知 layer
更新顯示的內容。
同步顯示的部分到這裡已經很清楚了,我們更關心的其實還是非同步繪製的部分,因為這部分才是 ASDK 提升效率的關鍵;而這就要從獲取 displayBlock
的方法開始瞭解了。
displayBlock 的構建
displayBlock
的建立一般分為三種不同的方式:
- 將當前檢視的子檢視壓縮成一層繪製在當前頁面上
- 使用
- displayWithParameters:isCancelled:
返回一個UIImage
,對影象節點ASImageNode
進行繪製 - 使用
- drawRect:withParameters:isCancelled:isRasterizing:
在 CG 上下文中繪製文位元組點ASTextNode
這三種方式都通過 ASDK 來優化檢視的渲染速度,這些操作最後都會扔到後臺的併發執行緒中進行處理。
下面三個部分的程式碼經過了刪減,省略了包括取消繪製、通知代理、控制併發數量以及用於除錯的程式碼。
柵格化子檢視
如果當前的檢視需要柵格化子檢視,就會進入啟用下面的構造方式建立一個 block,它會遞迴地將子檢視繪製在父檢視上:
- (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];
}複製程式碼
在壓縮檢視層級的過程中就會呼叫 -[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]
方法,獲取子檢視的所有 displayBlock
,在得到 UIGraphicsBeginImageContextWithOptions
需要的引數之後,建立一個新的 context,執行了所有的 displayBlock
將子檢視的繪製到當前圖層之後,使用 UIGraphicsGetImageFromCurrentImageContext
取出圖層的內容並返回。
-[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]
的實現還是有些繁瑣的,它主要的功能就是使用 Core Graphics 進行繪圖,將背景顏色、仿射變換、位置大小以及圓角等引數繪製到當前的上下文中,而且這個過程是遞迴的,直到不存在或者不需要繪製子節點為止。
繪製圖片
displayBlock
的第二種繪製策略更多地適用於圖片節點 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:
的執行返回一個圖片,不過這裡的繪製也離不開 Core Graphics 的一些 C 函式,你會在 -[ASImageNode displayWithParameters:isCancelled:]
中看到對於 CG 的運用,它會使用 drawParameters
來修改並繪製自己持有的 image
物件。
使用 CG 繪圖
文字的繪製一般都會在 - drawRect:withParameters:isCancelled:isRasterizing:
進行,這個方法只是提供了一個合適的用於繪製的上下文,該方法不止可以繪製文字,只是在這裡繪製文字比較常見:
- (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];
}複製程式碼
上述程式碼跟第一部分比較像,區別是這裡不會柵格化子檢視;程式碼根據情況會決定是否重新開一個新的上下文,然後通過 - drawRect:withParameters:isCancelled:isRasterizing:
方法實現繪製。
管理繪製事務
ASDK 提供了一個私有的管理事務的機制,由三部分組成 _ASAsyncTransactionGroup
、_ASAsyncTransactionContainer
以及 _ASAsyncTransaction
,這三者各自都有不同的功能:
_ASAsyncTransactionGroup
會在初始化時,向 Runloop 中註冊一個回撥,在每次 Runloop 結束時,執行回撥來提交displayBlock
執行的結果_ASAsyncTransactionContainer
為當前CALayer
提供了用於儲存事務的容器,並提供了獲取新的_ASAsyncTransaction
例項的便利方法_ASAsyncTransaction
將非同步操作封裝成了輕量級的事務物件,使用 C++ 程式碼對 GCD 進行了封裝
從上面的小節中,我們已經獲取到了用於繪製的 displayBlock
,然後就需要將 block 新增到繪製事務中:
- (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
例項的過程,這個例項會包含在一個 layer
的雜湊表中,最後呼叫的例項方法 -[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]
會把用於繪製的 displayBlock
新增到後臺並行佇列中:
+ (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;
}複製程式碼
這個佇列是一個並行佇列,並且優先順序是 DISPATCH_QUEUE_PRIORITY_HIGH
,確保 UI 的渲染會在其它非同步操作執行之前進行,而 -[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]
中會初始化 ASDisplayNodeAsyncTransactionOperation
的例項,然後傳入 completionBlock
,在繪製結束時回撥:
- (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
方法是一個 C++ 方法,它會向 ASAsyncTransactionQueue::Group
中派發一個 block,這個 block 中會執行 displayBlock
,然後將結果傳給 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
其實現其實就是對 GCD 的封裝,同時新增一些最大併發數、執行緒鎖的功能。通過 dispatch_async
將 block 分發到 queue
中,立刻執行 block,將資料傳回 ASDisplayNodeAsyncTransactionOperation
例項。
回撥
在 _ASAsyncTransactionGroup
呼叫 mainTransactionGroup
類方法獲取單例時,會通過 +[_ASAsyncTransactionGroup registerTransactionGroupAsMainRunloopObserver]
向 Runloop 中註冊回撥:
+ (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);
}複製程式碼
上述程式碼會在即將退出 Runloop 或者 Runloop 開始休眠時執行回撥 _transactionGroupRunLoopObserverCallback
,而這個回撥方法就是這一條主線的入口:
static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
ASDisplayNodeCAssertMainThread();
_ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info;
[group commit];
}複製程式碼
上一節中只是會將繪製程式碼提交到後臺的併發程式中,而這裡才會將結果提交,也就是在每次 Runloop 迴圈結束時開始繪製內容,而 -[_operationCompletionBlock commit]
方法的呼叫棧能夠幫助我們理解內容是如何提交的,又是如何傳回 node
持有的 layer
的:
-[_ASAsyncTransactionGroup commit]
-[_ASAsyncTransaction commit]
ASAsyncTransactionQueue::GroupImpl::notify(dispatch_queue_t, dispatch_block_t)
_notifyList.push_back(GroupNotify)複製程式碼
-[_ASAsyncTransactionGroup commit]
方法的呼叫完成了對繪製事務的提交,而在 -[_ASAsyncTransaction commit]
中會呼叫 notify
方法,在上一節中的 displayBlock
執行結束後呼叫這裡傳入的 block 執行 -[_ASAsyncTransaction completeTransaction]
方法:
- (void)commit {
ASDisplayNodeAssertMainThread();
__atomic_store_n(&_state, ASAsyncTransactionStateCommitted, __ATOMIC_SEQ_CST);
_group->notify(_callbackQueue, ^{
ASDisplayNodeAssertMainThread();
[self completeTransaction];
});
}複製程式碼
我們按照時間順序來分析在上面的 block 執行之前,方法是如何呼叫的,以及 block 是如何被執行的;這就不得不回到派發繪製事務的部分了,在 ASAsyncTransactionQueue::GroupImpl::schedule
方法中,使用了 dispatch_async
將派發 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
執行之後,會呼叫的 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);
}
}
}複製程式碼
這裡終於執行了在 - 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:]
方法執行了回撥將 displayBlock
執行的結果傳回了 CALayer:
- (void)callAndReleaseCompletionBlock:(BOOL)canceled; {
if (_operationCompletionBlock) {
_operationCompletionBlock(self.value, canceled);
self.operationCompletionBlock = nil;
}
}複製程式碼
也就是在 -[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]
方法中構建的 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];
}
};複製程式碼
這一部分進行的大量的資料傳遞都是通過 block 進行的,從 Runloop 中對事務的提交,以及通過 notify
方法加入的 block,都是為了最後將繪製的結果傳回 CALayer
物件,而到這裡可以說整個 ASDK 對於檢視內容的繪製過程就結束了。
總結
ASDK 對於繪製過程的優化有三部分:分別是柵格化子檢視、繪製影象以及繪製文字。
它攔截了檢視加入層級時發出的通知 - willMoveToWindow:
方法,然後手動呼叫 - setNeedsDisplay
,強制所有的 CALayer
執行 - display
更新內容;
然後將上面的操作全部拋入了後臺的併發執行緒中,並在 Runloop 中註冊回撥,在每次 Runloop 結束時,對已經完成的事務進行 - commit
,以圖片的形式直接傳回對應的 layer.content
中,完成對內容的更新。
從它的實現來看,確實解決了很多昂貴的 CPU 以及 GPU 操作,有效地加快了檢視的繪製和渲染,保證了主執行緒的流暢執行。
References
- How VSync works, and why people loathe it
- 腦洞大開:為啥幀率達到 60 fps 就流暢?
- iOS 保持介面流暢的技巧
- CADiplayLink Class Reference - Developer- Apple
- CPU vs GPU · iOS 核心動畫高階技巧
- 理解 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
Source: draveness.me/asdk-render…