使用 ASDK 性能調優 - 提升 iOS 界面的渲染性能

uiyh1163 8年前發布 | 49K 次閱讀 iOS開發 Objective-C開發

這一系列的文章會從幾個方面對 ASDK 在性能調優方面策略的實現進行分析,幫助讀者理解 ASDK 如何做到使復雜的 UI 界面達到 60 FPS 的刷新頻率的;本篇文章會從視圖的渲染層面講解 ASDK 對于渲染過程的優化并對 ASDK 進行概述。

在客戶端或者前端開發中,對于性能的優化,尤其是 UI,往往都不是最先考慮的問題。

因為在大多數場景下,使用更加復雜的高性能代碼替代可用的代碼經常會導致代碼的可維護性下降,所以更需要我們開發者對優化的時間點以及原因有一個比較清楚的認識,避免過度優化帶來的問題。

對 iOS 開發比較熟悉的開發者都知道,iOS 中的性能問題大多是阻塞主線程導致用戶的交互反饋出現可以感知的延遲。

詳細說起來,大體有三種原因:

  1. UI 渲染需要時間較長,無法按時提交結果;
  2. 一些需要 密集計算 的處理放在了主線程中執行,導致主線程被阻塞,無法渲染 UI 界面;
  3. 網絡請求由于網絡狀態的問題響應較慢,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°,所以光可以通過;
  • 在加入電壓之后,光沿著液晶分子的間隙直線前進,被濾光板擋住。

如果你可以KX上網,相信下面的視頻會更好得幫助你理解 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)是由 非死book 開源的一個 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 的創建一般分為三種不同的方式:

  1. 將當前視圖的子視圖壓縮成一層繪制在當前頁面上
  2. 使用 - displayWithParameters:isCancelled: 返回一個 UIImage,對圖像節點 ASImageNode 進行繪制
  3. 使用 - 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];
}

 

來自:https://www.sdk.cn/news/4930

 

 本文由用戶 uiyh1163 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!