从 Graver 源码再来看异步渲染

3,612 阅读12分钟

async-render

Graver 探究

Graver 是美团 18 年底开源的 iOS 异步渲染框架,因为一些争议最近在 GitHub 上取消开源,不过有 fork 过的仓库我们还是可以看下其实现细节。

Graver 开源的介绍文章可以参考 美团开源Graver框架:用“雕刻”诠释iOS端UI界面的高效渲染, 从中可以看到其主推的几大特点:

  • CPU 占用低, 性能表现优异
  • 异步化
  • 性能消耗的边界成本低
  • 渲染速度快

模块关系

Graver 源码中主要有四个部分:

  • AsyncDraw 异步渲染的核心模块,包括渲染分工的类继承关系
  • AttributedItem 负责串联文本,图片等展示信息的对象,将会绑定在 AsyncDraw 模块中的 View 上来进行渲染
  • CoreText 文字渲染,布局,响应事件的核心实现,基于 CoreText Framework
  • PreLayout 使用场景的一些定义,笔墨较少

这篇文章我们将聚焦其异步渲染相关的内容。

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的异步渲染和布局引擎