從 Graver 源碼再來看異步渲染

async-render

Graver 探究

Graver 是美團 18 年末開源的 iOS 異步渲染框架,由於一些爭議最近在 GitHub 上取消開源,不過有 fork 過的倉庫咱們仍是能夠看下其實現細節。html

Graver 開源的介紹文章能夠參考 美團開源Graver框架:用「雕刻」詮釋iOS端UI界面的高效渲染, 從中能夠看到其主推的幾大特色:node

  • CPU 佔用低, 性能表現優異
  • 異步化
  • 性能消耗的邊界成本低
  • 渲染速度快

模塊關係

Graver 源碼中主要有四個部分:git

  • AsyncDraw 異步渲染的核心模塊,包括渲染分工的類繼承關係
  • AttributedItem 負責串聯文本,圖片等展現信息的對象,將會綁定在 AsyncDraw 模塊中的 View 上來進行渲染
  • CoreText 文字渲染,佈局,響應事件的核心實現,基於 CoreText Framework
  • PreLayout 使用場景的一些定義,筆墨較少

這篇文章咱們將聚焦其異步渲染相關的內容。github

AysncDraw

異步渲染的核心模塊,其中視圖類從父到子主要爲 WMGAsynceDrawViewWMGCanvasViewWMGCanvaseControlWMGMixedView數組

WMGAsyncDrawView

WMGAsynceDrawView 頂層類,繼承自 UIView, 定義了一些基礎屬性和行爲,好比 layerClass 使用自定義的 WMGAsyncDrawLayer,異步繪製的隊列,繪製的策略 (同步或者異步) 等。緩存

核心的繪製則是由 drawRect: 以及 _displayLayer:rect:drawingStarted:drawingFinished:drawingInterrupted: 完成。安全

- (void)drawRect:(CGRect)rect
{
    [self drawingWillStartAsynchronously:NO];
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    if (!context) {
        WMGLog(@"may be memory warning");
    }
    
    [self drawInRect:self.bounds withContext:context asynchronously:NO userInfo:[self currentDrawingUserInfo]];
    [self drawingDidFinishAsynchronously:NO success:YES];
}
複製代碼

drawRect: 只是調用了一個等待子類實現的 drawInRectXXX 方法,同時調用了 will 和 did 渲染完成的回調,注意這裏是非異步渲染時繪製的流程,若是異步渲染須要顯式調用 setNeedsDisplayAsync,而後其會調用 [self.layer setNeedsDisplay] 方法來觸發 CALayerDelegate 的 displayLayer: 方法。多線程

而實際進行 layer 繪製的 pipeline 較長,能夠分爲幾大步:併發

  • 比較 layer 的哨兵 drawingCount 來防止異步致使的繪製上下文異常
[layer increaseDrawingCount];
NSUInteger targetDrawingCount = layer.drawingCount;
if (layer.drawingCount != targetDrawingCount)
{
    failedBlock();
    return;
}
複製代碼
  • 檢查渲染尺寸並開始調用上面非異步渲染也調用的 drawInRect:withContext:asynchronously:userInfo: 方法,交給子類渲染
CGSize contextSize = layer.bounds.size;
BOOL contextSizeValid = contextSize.width >= 1 && contextSize.height >= 1;
CGContextRef context = NULL;
BOOL drawingFinished = YES;
    
if (contextSizeValid) 
{
    UIGraphicsBeginImageContextWithOptions(contextSize, layer.isOpaque, layer.contentsScale);
    context = UIGraphicsGetCurrentContext();
    CGContextSaveGState(context);

    // ...
    drawingFinished = [self drawInRect:rectToDraw withContext:context asynchronously:drawInBackground userInfo:drawingUserInfo];
    CGContextRestoreGState(context);
}
複製代碼
  • 渲染完成生成位圖, 並在主線程設置爲 layer 的 backingImage
// 全部耗時的操做都已完成,但僅在繪製過程當中未發生重繪時,將結果顯示出來
if (drawingFinished && targetDrawingCount == layer.drawingCount)
{
    CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
    {
        // 讓 UIImage 進行內存管理
        UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
        void (^finishBlock)(void) = ^{
            // 因爲block可能在下一runloop執行,再進行一次檢查
            if (targetDrawingCount != layer.drawingCount)
            {
                failedBlock();
                return;
            }
            
            layer.contents = (id)image.CGImage;
            // ...
        }
        if (drawInBackground) dispatch_async(dispatch_get_main_queue(), finishBlock);
        else finishBlock();
    }
    
    // 一些清理工做: release CGImageRef, Image context ending
}
複製代碼

線程的處理上,繪製能夠指定在外部傳進來的隊列,不然就使用 global queue框架

- (dispatch_queue_t)drawQueue
{
    if (self.dispatchDrawQueue)
    {
        return self.dispatchDrawQueue;
    }
    return dispatch_get_global_queue(self.dispatchPriority, 0);
}
複製代碼

其餘視圖類

WMGCanvasView 繼承自 WMGAsyncDrawView, 主要負責圓角,邊框,陰影和背景圖片的繪製,繪製經過 CoreGraphics API 。

WMGCanvasControl 繼承自 WMGCanvasView,在這層處理事件響應,自實現了一套 Target-Action 模式,重寫了 touchesBegin/Moved/Cancelled/Moved 一系列方法,來進行響應狀態決議,而後將事件發給緩存的 targets 對象看可否響應指定的 control events 。

- (void)_sendActionsForControlEvents:(UIControlEvents)controlEvents withEvent:(UIEvent *)event
{
    for(__WMGCanvasControlTargetAction *t in [self _targetActions])
    {
        if(t.controlEvents == controlEvents)
        {
            if(t.target && t.action)
            {
                [self sendAction:t.action to:t.target forEvent:nil];
            }
        }
    }
}
複製代碼

WMGMixedView 則是上層視圖,屬性僅有水平/垂直對齊方式,行數和繪製內容 attributedItem 。drawInRect 中則根據對齊方式來決定繪製文字位置, 而後調用 textDrawer 來進行文字渲染,若是其中有圖片則會讀取後直接經過 drawInRect: 方法來渲染圖片(經過 TextDrawer 的 delegate)。

討論

咱們能夠經過 demo 來查看其實際渲染中的圖層:

screen-shot

Graver 經過將全部子視圖/圖層壓扁的形式來減小圖層的層級,比較適用於靜態內容渲染的場景,但失去了視圖/圖層樹,也相應就失去了樹形結構的靈活性,這個 Demo 中若是手動點擊 cell,會致使整個 cell content 重繪,出現圖片閃屏的狀況。而在不使用 Graver 狀況下,點擊 cell 只須要 selectionView 或其餘點擊區域去作出相關響應反饋便可,因此視圖層級的劃分能夠幫助咱們更細粒度的去進行佈局,繪製和點擊事件的處理。

另外在未開啓異步渲染時,更多的依賴 drawRect: 方法也會帶來必定的內存消耗,尤爲是較大區域的繪製。

官方在解讀其相應的性能提高以下圖所示:

perfomance

FPS 提高在 2 到 10 幀之間,若是能穩定在 57 到 60 是一個不錯的數據,FPS 的提高也是主要得益於異步隊列處理渲染。不過某些場景下好比長列表的異步渲染雖然帶來一些性能提高但也會面臨一些其餘的體驗問題,好比上面提到的交互重繪範圍,還有就是快速滾動狀況下的帶來的視覺延遲效果也須要其餘手段來彌補,這點上後面聊到的 Texture 在列表處理上會抽象出預渲染區域。

pre-render

美團在 19 年末分享的 美團 iOS 端開源框架 Graver 在動態化上的探索與實踐 中也提到了美團動態化框架 MTFlexbox 對接 Graver 時遇到的問題 :

  • 如何基於位圖進行事件處理
  • 動效等沒法依託 Graver 進行圖文渲染,須要考慮跨渲染引擎融合,同時須要決議是否將繪製粒度拆分
  • 極端場景下的繪製效率瓶頸

經過異步渲染繪製位圖來實現的狀況下,存在單一併發渲染任務計算邏輯繁重的問題,從用戶體驗層面看容易形成「白屏」現象。爲解決該問題,將視圖卡片渲染過程分解,進行增量渲染,採用漸進式的方式減小空白頁面等待時間。

因此總的來看, Graver 在做爲第三方庫接入時,比較適用於部分靜態區域圖文組合的繪製,不適用於大規模的使用。

YYAsyncLayer

YYAsyncLayer 比較老,屬於 YYKit 其中一部分,其核心就是同名類,該類繼承自 CALayer,只專一於異步渲染的 layer 實現。

來看其 _displayAsync: 方法

  1. 異步繪製狀況下,獲取渲染相關屬性,作哨兵,尺寸檢查,決定是否要釋放資源
// Sentinel 實際爲一個能夠原子自增的 int32_t
YYSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)(void) = ^BOOL() {
    return value != sentinel.value;
};
CGSize size = self.bounds.size;
BOOL opaque = self.opaque;
CGFloat scale = self.contentsScale;
CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;
if (size.width < 1 || size.height < 1) {
    CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
    self.contents = nil;
    if (image) {
        dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{
            CFRelease(image);
        });
    }
    if (task.didDisplay) task.didDisplay(self, YES);
    CGColorRelease(backgroundColor);
    return;
}
複製代碼
  1. 異步開始繪製圖片, 主要繪製顏色,不斷的對哨兵進行檢查
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
    
if (opaque) { 
    // ... 背景顏色繪製
}
task.display(context, size, isCancelled);
// ... sentinel check
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 切換到主線程渲染
dispatch_async(dispatch_get_main_queue(), ^{
    // ... sentinel check
    self.contents = (__bridge id)(image.CGImage);
    if (task.didDisplay) task.didDisplay(self, YES);
});
複製代碼

非異步渲染實現與其相似,這裏省略。

能夠看出 Graver 應該參考了 YY 的異步實現,同時在上層抽象出繼承鏈來分攤不一樣職責。YYAsyncLayer 在 YYKit 的位置相對底層,依賴其的 YYText 則會實現協議,完成上層渲染的實現。

同時,YYAsyncLayer 中還抽象了 Transaction 的概念,在第一次調用時向主線程 RunLoop 註冊優先級低於 CoreAnimation 的 Observer ,

static void YYTransactionSetup() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        transactionSet = [NSMutableSet new];
        CFRunLoopRef runloop = CFRunLoopGetMain();
        CFRunLoopObserverRef observer;
        
        observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                           kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                           true,      // repeat
                                           0xFFFFFF,  // after CATransaction(2000000)
                                           YYRunLoopObserverCallBack, NULL);
        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    });
}
複製代碼

回調時遍歷當前全部未執行的 transaction 來觸發執行。

線程方面,其默認會讀取 YYDispatchQueue 線程池的隊列,若是沒有該模塊則根據硬件狀況來簡單的實現一個線程池,經過 number % capacity 的方式來分配負載。

Texture

Texture (AsyncDisplayKit) 是 Facebook 開源的一個相對較重的視圖框架。其將視圖渲染元素抽象爲各類類型的 Node,框架決議如何來完成異步渲染及渲染優化,其也是當前使用最爲普遍的異步渲染相關框架。

ASDisplayNode

ASDisplayNode 是其全部上層 node 的基類,其定義了各個方面的基礎行爲,包括:

  • Life Cycle
  • Layout
  • Display
  • Coordinate System
  • Hierarchy
  • Visibility
  • States
  • Touch Events
  • Accessibility

這裏咱們仍是重點關注異步渲染的部分。

同步

咱們知道 UIKit components 線程不安全,多線程讀寫屬性會產生異常。而 ASDisplayNode 大部分容許異步訪問的屬性和方法都在其做用域加了鎖:

- (BOOL)rasterizesSubtree
{
  MutexLocker l(__instanceLock__);
  return _flags.rasterizesSubtree;
}

- (CGFloat)contentsScaleForDisplay
{
  MutexLocker l(__instanceLock__);

  return _contentsScaleForDisplay;
}
複製代碼

MutexLockertypedef std::lock_guard<Mutex> MutexLocker, Mutex 則爲 Texture 基於 std::recursive_mutex 封裝的遞歸鎖。

View or Layer

通常上層會根據視圖類型來決議使用 view 或者 layer ,是否 layerBacked 會存儲在 _flags 結構體中,view 或 layer 都是懶加載的,訪問 view 或者 layer 方法時纔會初始化,已訪問 view 爲例:

- (UIView *)view
{
  AS::UniqueLock l(__instanceLock__);

 // 若是是 layer backed 直接返回 nil
  ASDisplayNodeAssert(!_flags.layerBacked, @"Call to -view undefined on layer-backed nodes");
  BOOL isLayerBacked = _flags.layerBacked;
  if (isLayerBacked) {
    return nil;
  }

  if (_view != nil) {
    return _view;
  }

  if (![self _locked_shouldLoadViewOrLayer]) {
    return nil;
  }
  
  // 加載視圖須要在主線程
  ASDisplayNodeAssertMainThread();
  [self _locked_loadViewOrLayer];
  
 // ... layout, 添加到節點樹,狀態更新

  return _view;
}
複製代碼

再展開看下 _locked_loadViewOrLayer

- (void)_locked_loadViewOrLayer
{
  // 判斷是否爲 layer backed
  if (_flags.layerBacked) {
    _layer = [self _locked_layerToLoad];
    static int ASLayerDelegateAssociationKey;

    // 因爲 layer 的生命週期也許要比 node 長,因此須要將 delegate 使用 proxy 包裝成 weak 
    ASWeakProxy *instance = [ASWeakProxy weakProxyWithTarget:self];
    _layer.delegate = (id<CALayerDelegate>)instance;
    objc_setAssociatedObject(_layer, &ASLayerDelegateAssociationKey, instance, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  } else {
   // 初始化 view 並作一些特殊 handling
    _view = [self _locked_viewToLoad];
    _view.asyncdisplaykit_node = self;
    _layer = _view.layer;
  }
  // 將 layer 和 node 經過關聯對象聯繫起來
  _layer.asyncdisplaykit_node = self;
  
  self._locked_asyncLayer.asyncDelegate = self;
}
複製代碼

RunLoop Queue

ASDisplayNode 在 needsDisplay 時會將本身加到一個 renderQueue 中,類型爲 ASRunLoopQueue

ASRunLoopQueue 是一個支持在指定 runloop 下執行任務隊列的類,全局只有一個該隊列實例。其初始化方法以下

- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop retainObjects:(BOOL)retainsObjects handler:(void (^)(id _Nullable, BOOL))handlerBlock
{
  if (self = [super init]) {
    _runLoop = runloop;
    NSPointerFunctionsOptions options = retainsObjects ? NSPointerFunctionsStrongMemory : NSPointerFunctionsWeakMemory;
    _internalQueue = [[NSPointerArray alloc] initWithOptions:options];
    _queueConsumer = handlerBlock;
    _batchSize = 1;
    _ensureExclusiveMembership = YES;
    /// ...
    unowned __typeof__(self) weakSelf = self;
    void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
      [weakSelf processQueue];
    };
    _runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, handlerBlock);
    CFRunLoopAddObserver(_runLoop, _runLoopObserver,  kCFRunLoopCommonModes);   
    /// ...
    _runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext);
    CFRunLoopAddSource(runloop, _runLoopSource, kCFRunLoopCommonModes);
}
複製代碼

每次 enqueue node 時會先從內部隊列查找是否已經存在該對象,若是不存在,則添加到內部隊列並經過以前註冊的 source 喚醒 RunLoop

CFRunLoopSourceSignal(_runLoopSource);
CFRunLoopWakeUp(_runLoop);
複製代碼

每次 RunLoop 在 beforeWaiting 會調時,隊列會出隊一個 (或者多個取決於 batchSize, 默認爲 1) item 來執行,執行時 node 會遞歸地讓本節點及其孩子節點開始佈局和渲染。

display

_ASDisplayLayer 在 display 時會調用 delegate (ASDisplayNode) 的 displayAsyncLayer:asynchronously: 方法, 其方法實如今 ASDisplayNode + AsyncDisplay 的分類中,大概流程以下:

  1. 準備取消 block,當哨兵數值不一致或者 node 已被釋放時 cancel
uint displaySentinelValue = ++_displaySentinel;
__weak ASDisplayNode *weakSelf = self;
isCancelledBlock = ^BOOL{
  __strong ASDisplayNode *self = weakSelf;
  return self == nil || (displaySentinelValue != self->_displaySentinel.load());
};
複製代碼
  1. 開始 display 流程,首先準備相關參數和一些檢查 (好比 bounds 是否爲空)
  2. 生成 displayBlocks,遞歸將 sublayer 的渲染 block 加到 displayBlocks 數組
  3. 須要柵格化時執行 displayBlocks,生成圖片
// WWDC2018 蘋果推薦從 iOS10 開始使用 UIGraphicsImageRender API
if (AS_AVAILABLE_IOS_TVOS(10, 10)) {
    if (ASActivateExperimentalFeature(ASExperimentalDrawingGlobal)) {
     
      // ... 初始化並緩存 defaultFormat, opaqueFormat
      UIGraphicsImageRendererFormat *format;
      /// ... 配置 format 好比 scale , opaque
      
      return [[[UIGraphicsImageRenderer alloc] initWithSize:size format:format] imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
        ASDisplayNodeCAssert(UIGraphicsGetCurrentContext(), @"Should have a context!");
        // work block 即爲 display block,這個宏會調用其執行
        PERFORM_WORK_WITH_TRAIT_COLLECTION(work, traitCollection)
      }];
    }
}

/// 10 之前系統使用舊的 UIGraphicsImage API
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
PERFORM_WORK_WITH_TRAIT_COLLECTION(work, traitCollection)
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
複製代碼
  1. 若是不須要柵格化則在區分是否建立 CGContext 來決議如何繪製圖片並返回
  2. 非異步渲染直接設置 layer 的 contents,異步則構造 _ASAsyncTransaction 對象並添加到異步隊列中執行 operation,該隊列優先級爲 DISPATCH_QUEUE_PRIORITY_HIGH 的 global 全局隊列
if (asynchronously) {
    CALayer *containerLayer = layer.asyncdisplaykit_parentTransactionContainer ? : layer;
    _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
    [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
}
複製代碼
  1. operation 會被添加到 operation 隊列中 schedule 執行
// 根據核心數和主線程 RunLoop mode 來決議最多線程數
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, ^{
      std::unique_lock<std::mutex> lock(q._mutex);
      // 執行隊列裏的 display block, 標記線程數
      while (!entry._operationQueue.empty()) {
        Operation operation = entry.popNextOperation(respectPriority);
        lock.unlock();
        if (operation._block) {
          operation._block();
        }
        operation._group->leave();
        operation._block = nil; 
        lock.lock();
      }
      --entry._threadCount;
      
      if (entry._threadCount == 0) {
        NSCAssert(entry._operationQueue.empty() || entry._operationPriorityMap.empty(), @"No working threads but operations are still scheduled");
        q._entries.erase(queue);
      }
    });
}
複製代碼
  1. 每一個 operation 繪製完成時會調用 operation._group->leave() 這時會 notify 等待的 _condition,而後執行 completion block,completion block 會在主線程完成 layer 寄宿圖的設置
- (void)waitUntilComplete
{
  ASDisplayNodeAssertMainThread();
  if (self.state != ASAsyncTransactionStateComplete) {
    if (_group) {
      _group->wait();
      
      if (self.state == ASAsyncTransactionStateOpen) {
        [_ASAsyncTransactionGroup.mainTransactionGroup commit];
        NSAssert(self.state != ASAsyncTransactionStateOpen, @"Transaction should not be open after committing group");
      }
      [self completeTransaction];
    }
  }
}
複製代碼

上面的 completion block 也有可能在每一個運行循環 beforeWaiting | Exit 時機時執行,_ASAsyncTransactionGroup 在運行循環註冊了一個在 CA Transaction 以後的觀察者,該回調會遍歷 _ASAsyncTransaction 對象,判斷其狀態並執行 complete block 。

到這 Texture 異步提交渲染事務的主流程就結束了,這也只是 Texture 框架的一部分,其上層徹底的對照 UIKit 實現了一套支持異步渲染的 UI 框架, 其佈局引擎的實現也有諸多能夠探究和學習的地方。

texture-layout

更多內容能夠去 GitHub 上查看其源碼

其餘一些 Code Snippets

ASDisplayNode 的 +initialize 方法,會檢查子類是否重寫了不容許重寫的方法

initial-1

同時爲了不寫方法體爲空的分類中聲明的方法,也經過 initialize 中動態添加

initial-2

總結

在一般的應用場景下,主線程操做 UI 已經可以保證渲染的性能和體驗,在某些極端場景下,咱們可能須要考慮針對主線程任務的治理和渲染細節的優化。若是渲染效率或者體驗上依舊不能達到要求,則能夠考慮拆分組件選擇異步渲染策略。Graver,YYAsyncLayer,Texture 都是咱們能夠借鑑的框架,使用策略咱們能夠根據實際的場景和各自框架的特色來綜合權衡。

Reference:

  1. Texture
  2. YYAsyncLayer
  3. 美團開源Graver框架:用「雕刻」詮釋iOS端UI界面的高效渲染
  4. 美團 iOS 端開源框架 Graver 在動態化上的探索與實踐
  5. Texture的異步渲染和佈局引擎
相關文章
相關標籤/搜索