AsyncDisplayKit笔记

本文深入解析AsyncDisplayKit框架,介绍其核心组件Node的功能与使用方法,包括init、didLoad及layoutSpecThatFits等关键方法。同时,文章详细阐述了AsyncDisplayKit的工作原理,包括图像处理、布局计算和绘制流程,以及如何优化CPU与GPU资源,实现流畅的界面渲染。

1.Node的几个常用方法及用途

  1. 在一个节点容器的上下文之外使用一个节点有一些弊端,因为所有的节点都具有当前界面状态的概念 ASInterfaceState

  2. ASInterfaceState的更新由ASRangeController控制而ASRangeController又由所有的节点容器在内部创建和维护,一个节点的状态有大概三种:Preload、Display、Visible

    -init:
    initNodeBlocks这个方法会在后台线程调用,因为没有其他方法在init完成之前运行,所以这个不需要加锁。 init方法必须能够在任何队列上调用、不能在节点初始化方法中初始任何UIKit对象、不能在这个方法中添加手势、node.layer,node.view.x相关操作。

    -didLoad:
    当后台视图初始化完成时,会被调用,保证是在主线程,可以在这里执行UIKit的代码。

    -layoutSpecThatFits
    该方法定义节点的布局,并在后台线程上进行了大量的计算,修改布局的地方,这个地方同样也不能使用node.view、node.layer以及他们的属性,此外,最好不要在这个方法中创建其他节点。

    -layout
    1.在此方法中调用super将会使用layoutSpec对象计算布局,所有子节点都将计算其size和position。

    2.layout在概念上类似于UIViewController 的-viewWillLayoutSubviews,这是一个更改hidden属性、修改view属性、设置背景颜色的好地方。

    3.layout中计算子节点的size是很精确的,例如collectionNode可以铺满屏幕,这种情况不被ASLayoutSpec很好的支持,此时简单的做法就是在这个方法中手动设定frame。

2.AsyncDisplayKit的原理实现

AsyncDisplayKit是Facebook开源的一套用于iOS界面流畅的异步绘制UI的框架,其核心是对CPU以及GPU的优化,CPU方面是将一些高消耗运算放在后台线程中进行,减轻主线程负担。CGU方面则是将每一帧的内容渲染成一张texture,这个工作在后台线程完成。

AsyncDisplayKit的基本单元是node,node是UIView以及对应Layer的抽象,与UIView最大的区别是Node是线程安全的,并且可以设置对应的Node内部层次后在后台线程运行,UIView将大量的耗时操作全部都放在了主线程上进行

ASDisplayNode是线程安全的,可以在后台线程创建和修改,Node刚创建时,并不会在内部创建UIView和CALayer,直到主线程第一次访问view或者layer属性时,它才会在内部生成对应的对象。当它的属性(frame、transform)改变时,并不会立刻同步到其持有的view或者layer上,而是把这些改变的属性保存到内存的一个中间变量上,等某一个时机一次性设置到内部的view或layer上,实际绘制代码在ASDisplayNode+AsyncDisplay.mm中。

主线程中比较耗时的主要是图像、布局以及绘制

  1. 图像

    a.解压
    UIImage对其内部图像格式的解压发生在即将将图片交给GPU渲染的时候调用drawInRect。 函数或者将UIView可见时,UIImage会将其内部图像格式如PNG,JPEG进行解压。AsyncDisplayKit就是采用后面那个方式将UIView预先放置如一个frame为空的workingView中以达到将view内部的image进行预解压目的。

    b.处理
    图像一般会进行blend运算、strech、crop,这些工作其实可以在GPU中处理,但是UIKit并没有提供此类的API,所以一般我会用CoreGraphic来做,但是CoreGraphic 是CPU drawing比较耗时的。AsyncDisplayKit将这些图像处理放在工程线程中来处理,虽然而是CPU drawing,但是不会影响到UI的顺滑响应。

  2. 布局

    以ASTableView为例,TableView的UI布局需要计算每行的高度,然后计算行内元素的布局,将行插入到TableView中。TableView又是scrollView,需要滚动,一旦行的生成和渲染比较慢,必然会影响到滑动时的流畅体验。这个过程只有将行插入到TableView中需要在UI线程中进行,AsyncDisplayKit在子线程分批次计算行以及其子元素的布局,计算好之后通过dispatch_group_notify通知UI线程将row插入到View中。AsyncDisplayKit还考虑到设备的CPU核数,根据核数来分批次计算row的大小

  3. 绘制

    a.AsyncDisplayKit另一个强大之处在于将UI CALayer 的backing image的绘制放入到工作线程,UIView的真正展现内容在于其CALayer的contents属性,而contents属性对应一个Backing image(内存位图)。默认情况下UIView一旦出现,其会自动创建一个Backing image。

    b.AsyncDisplayKit通过CALayer的delegate控制baking image,并通过Core Graphic的方式在工作线程上将View以及其子节点绘制到backing image上,绘制好之后在UI线程上将这个backing image传给CALayer的contents,然后将CALayer的渲染树交给GPU。绘制以组的形式执行减少了Graphic context 的切换。

    c.通过transaction的方式管理dispatch_group之间的关系,并且只有在UI线程处于闲置状态或退出时才将transaction commit 并将backing image 赋给CALayer的contents。

核心技术

1.iOS的显示系统是由VSync信号驱动,VSync信号由硬件时钟生成,每秒发出60次,iOS图形服务接收到VSync信号后,会通过IPC通知到App内。App的Runloop在启动后会注册对应的CFRunLoopSource通过mach_port接收传过来的信号通知,随后Source的回调会驱动整个App的动画和显示。

2.CoreAnimation在Runloop中注册了一个Observer,监听BeforeWaiting和Exit事件.当一个触摸事件到来时,Runloop被唤醒,app会执行一些操作(比如创建或调整视图),这些操作都会被CALayer捕获,并通过CATransaction提交到一个中间状态去。当上面的所有操作结束后,Runloop即将进入休眠或退出时,关注该事件的Observer会得到通知,在回调中就会把所有的中间状态合并提交到GPU去显示,如果有动画,CALayer会通过DisplayLink 等机制多次触发相关流程。

3.AsyncDisplayKit在此处也模拟了Core这个机制,所有针对ASNode的修改和提交,总有些任务是必须放在主线程的。当主线这个任务的时候 ,ASNode会把任务封装到ASAsyncTransaction并提交到一个全局的容器中。

AsyncDisplayKit源码阅读笔记

在UIKit 中,将一个UIView加入到视图层级或调用setNeedDisplay后,将会触发绘制逻辑,而处于性能考虑并不会立即绘制,而仅仅是设置这个view为dirty,在下一次绘制事件到来的时候对view进行重绘。

在AsyncDisplayKit中也是从触发重绘的入口着手。

- (void)setNeedsDisplay
{
  _bridge_prologue_write;
  if (_hierarchyState & ASHierarchyStateRasterized) {
    //当前node已被栅格化时,ASDK不再重绘这个图层,转而不断向上寻找这个图层的父图层,
  } else {
    // 若当前的node的backingView/Layer尚已被初始化,,且setNeedsDisplay是在主线程被调用,
    //则简单得转发这个消息给自己的backingView/layer
    BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self);
    if (shouldApply) {
      _messageToViewOrLayer(setNeedsDisplay);
    } else {
    //若不满足上述条件则在node层设置一个标志位,标识自己需要重绘
      [ASDisplayNodeGetPendingState(self) setNeedsDisplay];
    }
    [self __setNeedsDisplay];
  }
}
复制代码

ASDK 保证node是线程安全的,所有消息都可以在非主线程调用。

- (void)__setNeedsDisplay {
  BOOL shouldScheduleForDisplay = NO; 
  {
    ASDN::MutexLocker l(__instanceLock__);
    BOOL nowDisplay = ASInterfaceStateIncludesDisplay(_interfaceState);
  
    if (_layer != nil && !_flags.synchronous && nowDisplay && [self _implementsDisplay]) {
      shouldScheduleForDisplay = YES;
    }
  }
  
  if (shouldScheduleForDisplay) {
    [ASDisplayNode scheduleNodeForRecursiveDisplay:self];
  }
}

复制代码

__setNeedsDisplay 主要是根据当前是否有重绘逻辑,判断是否需要重绘,如果当前的layer为空,或上面的nowDisplay为false,证明当前的node还未被展示到屏幕上,不需要走绘制的逻辑,因为在view/layer进入对应的hierarchy时,绘制逻辑自然会跑一遍,若当前的node不支持异步绘制,也不会走绘制的逻辑。 因此此时必定是在后台线程(因为有layer又在主线程的话,此消息已被转发给对应的backingView/layer);最后如果并没有override绘制逻辑或不需要栅格化子图层或不是图片展示类型的图层,则此处不会走异步绘制。

当if语句判断通过会执行下面的代码

- (void)_recursivelyTriggerDisplayAndBlock:(BOOL)shouldBlock

void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
   
复制代码

这两个方法干的比较重要的事就是调用了 [layer setNeedsDisplay]。

也就是说我们的绘制逻辑会最终走到layer层的display方法。

node的backingView/backingLayer进入相应的hierarchy时,如何处罚绘制。

不论是直接实例化——ASDisplayLayer或其子类,加入到node.layer,或者直接对封装好的node的layer层级进行操作,如[node.layer addSublayer: subnode.layer],都不会得到异步绘制的优化。

在_ASDisplayLayer.mm文件的- (void)display:(BOOL)asynchronously中可以看到,layer其实是将实际绘制任务交给自己的delegate完成,所以当直接实例化_ASDisplayLayer或其子类时,delegate为nil,自然没办法执行绘制逻辑。而对于第二种情况,则是因为ASDK仅仅在View层上加入了对层级变化的监听,即在_ASDisplayView.mm的-(void)willMoveToWindow方法作为绘制周期的入口,调用了[ASDisplayNode __enterHierarchy]。

- (void)__enterHierarchy
{
  ASDisplayNodeAssertMainThread();
  ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"Should not cause recursive __enterHierarchy");
  
  ASDN::MutexLocker l(_propertyLock);
  
  if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) {
    _flags.isEnteringHierarchy = YES;
    _flags.isInHierarchy = YES;
    
    if (_flags.shouldRasterizeDescendants) {
      [self _recursiveWillEnterHierarchy];
    } else {
      [self willEnterHierarchy];
    }
    _flags.isEnteringHierarchy = NO;

    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];
      }
    }
  }
}
复制代码

经过两种不同的逻辑,代码会进入到[_ASDisplayLayer display],并交给[strongAsyncDelegate displayAsyncLayer:self asynchronously:asynchronously] 执行真正的绘制逻辑,对于一般情况下delegate就是ASDisplayNode。

接下来了解何时调用displayBlock这个进行绘制的。

 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. 获取containerLayer,即当前layer层级中拥有transaction的最高层layer;

  2. 是调用获取containerLayer的asyncdisplaykit_asyncTransaction,这个可以看做是对CGD的封装。 在这个_ASAsyncTransactionContainerlass中的 asyncdisplaykit_asyncTransaction方法如下:

    - (_ASAsyncTransaction *)asyncdisplaykit_asyncTransaction
     {
      _ASAsyncTransaction *transaction = self.asyncdisplaykit_currentAsyncLayerTransaction;
      if (transaction == nil) {
      NSHashTable *transactions = self.asyncdisplaykit_asyncLayerTransactions;
      if (transactions == nil) {
       transactions = [NSHashTable hashTableWithOptions:NSPointerFunctionsObjectPointerPersonality];
       self.asyncdisplaykit_asyncLayerTransactions = transactions;
       }
     transaction = [[_ASAsyncTransaction alloc] initWithCallbackQueue:dispatch_get_main_queue() completionBlock:^(_ASAsyncTransaction *completedTransaction, BOOL cancelled) {
       [transactions removeObject:completedTransaction];
       //通知方法...
     }];
     [transactions addObject:transaction];
     self.asyncdisplaykit_currentAsyncLayerTransaction = transaction;
     
     //通知方法..
    }
    [[_ASAsyncTransactionGroup mainTransactionGroup] addTransactionContainer:self];
     return transaction;
    }
    复制代码

    2.1、asyncdisplaykit_asyncLayerTransactions是一个哈希表,用来存 _ASAsyncTransaction实例的。

    2.2、 asyncdisplaykit_currentAsyncTransaction表示当前事务的引用;

    在这个方法的最后把自己添加到一个事务组中。

3.displayBlock和completionBlock交个第二步getter获取的transaction这个事务管理。

具体来看看addOperationWithBlock这个方法的执行逻辑。

- (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
{
 [self _ensureTransactionData];

 ASAsyncTransactionOperation *operation = [[ASAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion];
 [_operations addObject:operation];
 _group->schedule(priority, queue, ^{
   @autoreleasepool {
     if (self.state != ASAsyncTransactionStateCanceled) {
       operation.value = block();
     }
   }
 });
 }
复制代码

3.1 completionBlock被保存到transaction的_operations数组中,而displayBlock被schedule在某一个执行,执行完的结果被存放在operation.value。

3.2 而_group->schedule则是调了这个函数void ASAsyncTransactionQueue::GroupImpl::schedule

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];

//1
Operation operation;
operation._block = block;
operation._group = this;
operation._priority = priority;
entry.pushOperation(operation);

//2
++_pendingOperations; // enter group

NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2;

if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode])
 --maxThreads;

if (entry._threadCount < maxThreads) { 

 bool respectPriority = entry._threadCount > 0;
 ++entry._threadCount;
 //3
 dispatch_async(queue, ^{
   ASDN::MutexLocker lock(q._mutex);
   
   // go until there are no more pending operations
   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; // the block must be freed while mutex is unlocked
     }
   }
   --entry._threadCount;
   
   if (entry._threadCount == 0) {
     NSCAssert(entry._operationQueue.empty() || entry._operationPriorityMap.empty(), @"No working threads but operations are still scheduled"); // this shouldn't happen
     q._entries.erase(queue);
   }
 });
}
}
复制代码

3.3 简单地封装block,这个方法中的Operation并不是上面方法中提到TransactionOperation混淆,_pendingOperations是一个标志位,代表当前需要排队的任务数量。然后在异步Block主要的逻辑就是要在对应的queue上执行的任务都执行一遍,最后调用operation._group->leave()。

3.4在leave这方法中

void ASAsyncTransactionQueue::GroupImpl::leave()
{
 std::lock_guard<std::mutex> l(_queue._mutex);
 --_pendingOperations;

 if (_pendingOperations == 0) {
   std::list<GroupNotify> notifyList;
   _notifyList.swap(notifyList);
   
   for (GroupNotify & notify : notifyList) {
     dispatch_async(notify._queue, notify._block);
   }
   
   _condition.notify_one();
   
   if (_releaseCalled) {
     delete this;
   }
 }
}
复制代码

当前的任务都完成后取了一个类型为GroupNotify的list,并且遍历并用GCD异步执行

程序走到这我们已经成功把displayBlock和completionBlock进行了提交,并且看到displayBlock是如何被封装执行,其返回值又如何传入 ASAsyncTransactionOperation实例中。

但是目前我们还不知道在什么地方执行completionBlock,从而将displayBlock的结果

4.接下来就看看_ASAsyncTransactionGroup.m中, 可以看到这个事务注册了对主线程Runloop的监听,而我们的_ASAsyncTransaction就是加入到这个group中的,器监听分别是kCFRunLoopBeforeWaiting 和 kCFRunLoopExit。

- (void)commit
{
  ASDisplayNodeAssertMainThread();

  if ([_containers count]) {
    NSHashTable *containersToCommit = _containers;
    _containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];

    for (id<ASAsyncTransactionContainer> container in containersToCommit) {
      // Note that the act of committing a transaction may open a new transaction,
      // so we must nil out the transaction we're committing first.
      _ASAsyncTransaction *transaction = container.asyncdisplaykit_currentAsyncTransaction;
      container.asyncdisplaykit_currentAsyncTransaction = nil;
      [transaction commit];
    }
  }
}
复制代码

事务组的commit就是遍历所有的事务,执行事务的commit。

在commit的方法中调用了_ASAsyncTransaction的commit,此处又出现了_group->notify方法,在主线程中执行completionBlock。

- (void)callAndReleaseCompletionBlock:(BOOL)canceled;
{
  if (_operationCompletionBlock) {
    _operationCompletionBlock(self.value, canceled);
 
    self.operationCompletionBlock = nil;
  }
}
复制代码

将displayBlock的结果在这里交给completionBlock,让它在主线程展示绘制的结果。

总结:ASyncDisplayKit的绘制流程被触发,会一方面监听MainRunLoop的事件,另一方面会执行绘制逻辑(递归地获取整个nodeTree的绘制代码,并在一个上下文中会被截图),同时在MainRunLoop运行到合适的时机,将后台绘制结果简单的赋值给layer.contents,完成界面的展示。

后台绘制耗时很长,而runloop的observerCallBack通知会在绘制完成前结束,runloop进入休眠后。由于绘制结束后的notify最终会在mainQueue中执行layer.contents = image的逻辑,因此runloop会因为需要操作主线程事务而被唤醒,以__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__的方式执行赋值操作。

转载于:https://juejin.im/post/5b59770c5188251b166f0638

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值