Graver 是美团 18 年末开源的 iOS 异步渲染框架,由于一些争议最近在 GitHub 上取消开源,不过有 fork 过的仓库咱们仍是能够看下其实现细节。html
Graver 开源的介绍文章能够参考 美团开源Graver框架:用“雕刻”诠释iOS端UI界面的高效渲染, 从中能够看到其主推的几大特色:node
Graver 源码中主要有四个部分:git
这篇文章咱们将聚焦其异步渲染相关的内容。github
异步渲染的核心模块,其中视图类从父到子主要为 WMGAsynceDrawView
,WMGCanvasView
,WMGCanvaseControl
,WMGMixedView
。数组
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:
方法。markdown
而实际进行 layer 绘制的 pipeline 较长,能够分为几大步:多线程
[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); } 复制代码
// 全部耗时的操做都已完成,但仅在绘制过程当中未发生重绘时,将结果显示出来 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 来查看其实际渲染中的图层:
Graver 经过将全部子视图/图层压扁的形式来减小图层的层级,比较适用于静态内容渲染的场景,但失去了视图/图层树,也相应就失去了树形结构的灵活性,这个 Demo 中若是手动点击 cell,会致使整个 cell content 重绘,出现图片闪屏的状况。而在不使用 Graver 状况下,点击 cell 只须要 selectionView 或其余点击区域去作出相关响应反馈便可,因此视图层级的划分能够帮助咱们更细粒度的去进行布局,绘制和点击事件的处理。
另外在未开启异步渲染时,更多的依赖 drawRect:
方法也会带来必定的内存消耗,尤为是较大区域的绘制。
官方在解读其相应的性能提高以下图所示:
FPS 提高在 2 到 10 帧之间,若是能稳定在 57 到 60 是一个不错的数据,FPS 的提高也是主要得益于异步队列处理渲染。不过某些场景下好比长列表的异步渲染虽然带来一些性能提高但也会面临一些其余的体验问题,好比上面提到的交互重绘范围,还有就是快速滚动状况下的带来的视觉延迟效果也须要其余手段来弥补,这点上后面聊到的 Texture 在列表处理上会抽象出预渲染区域。
美团在 19 年末分享的 美团 iOS 端开源框架 Graver 在动态化上的探索与实践 中也提到了美团动态化框架 MTFlexbox 对接 Graver 时遇到的问题 :
经过异步渲染绘制位图来实现的状况下,存在单一并发渲染任务计算逻辑繁重的问题,从用户体验层面看容易形成“白屏”现象。为解决该问题,将视图卡片渲染过程分解,进行增量渲染,采用渐进式的方式减小空白页面等待时间。
因此总的来看, Graver 在做为第三方库接入时,比较适用于部分静态区域图文组合的绘制,不适用于大规模的使用。
YYAsyncLayer 比较老,属于 YYKit 其中一部分,其核心就是同名类,该类继承自 CALayer
,只专一于异步渲染的 layer 实现。
来看其 _displayAsync:
方法
// 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; } 复制代码
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 (AsyncDisplayKit) 是 Facebook 开源的一个相对较重的视图框架。其将视图渲染元素抽象为各类类型的 Node,框架决议如何来完成异步渲染及渲染优化,其也是当前使用最为普遍的异步渲染相关框架。
ASDisplayNode
是其全部上层 node 的基类,其定义了各个方面的基础行为,包括:
这里咱们仍是重点关注异步渲染的部分。
咱们知道 UIKit components 线程不安全,多线程读写属性会产生异常。而 ASDisplayNode
大部分容许异步访问的属性和方法都在其做用域加了锁:
- (BOOL)rasterizesSubtree { MutexLocker l(__instanceLock__); return _flags.rasterizesSubtree; } - (CGFloat)contentsScaleForDisplay { MutexLocker l(__instanceLock__); return _contentsScaleForDisplay; } 复制代码
MutexLocker
为 typedef std::lock_guard<Mutex> MutexLocker
, Mutex 则为 Texture 基于 std::recursive_mutex
封装的递归锁。
通常上层会根据视图类型来决议使用 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; } 复制代码
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 会递归地让本节点及其孩子节点开始布局和渲染。
_ASDisplayLayer
在 display 时会调用 delegate (ASDisplayNode) 的 displayAsyncLayer:asynchronously:
方法, 其方法实如今 ASDisplayNode + AsyncDisplay 的分类中,大概流程以下:
uint displaySentinelValue = ++_displaySentinel; __weak ASDisplayNode *weakSelf = self; isCancelledBlock = ^BOOL{ __strong ASDisplayNode *self = weakSelf; return self == nil || (displaySentinelValue != self->_displaySentinel.load()); }; 复制代码
// 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; 复制代码
_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]; } 复制代码
// 根据核心数和主线程 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); } }); } 复制代码
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 框架, 其布局引擎的实现也有诸多能够探究和学习的地方。
更多内容能够去 GitHub 上查看其源码。
ASDisplayNode
的 +initialize 方法,会检查子类是否重写了不容许重写的方法
同时为了不写方法体为空的分类中声明的方法,也经过 initialize 中动态添加
在一般的应用场景下,主线程操做 UI 已经可以保证渲染的性能和体验,在某些极端场景下,咱们可能须要考虑针对主线程任务的治理和渲染细节的优化。若是渲染效率或者体验上依旧不能达到要求,则能够考虑拆分组件选择异步渲染策略。Graver,YYAsyncLayer,Texture 都是咱们能够借鉴的框架,使用策略咱们能够根据实际的场景和各自框架的特色来综合权衡。
Reference: