這一系列的文章會從幾個方面對 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)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 的渲染時間。
view-layer-cg-compare
-[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 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] 方法:
我們按照時間順序來分析在上面的 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::listnotifyList; _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 = ^(idvalue, 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: http://draveness.me/asdk-rendering