AsyncDisplayKit深度解析

在这里插入图片描述

AsyncDisplayKit是一款异步渲染的UI框架,我们知道UIKit的操作都是需要在主线程完成的,那么如何做到UI的异步渲染,使我我这个框架产生强烈好奇。因此我对其源码进行阅读并记录。

AsyncDisplayKit可以不费力地快速响应。可以分成三部分:

  1. 为什么快速响应是开发者应该关注的首要问题
  2. 框架的结构
  3. 怎么使用

一、为什么要快速响应

不熄火(no stalls)
即使app在处理一些其他事情比如网络加载,也应该快速响应用户的操作。
低延迟(no long delays)
尽管有很多事情要做,但是要快,不要等太久。
不模棱两可(no ambiguity)
有些任务需要快速响应。比如一些时候卡住了,你可能点击了没有反馈,你又需要再点一次,这样就很不友好。

查看web的响应调查:
在这里插入图片描述

响应从0.4s到0.9s会丢失20%用户。
每增加100ms会丢失1%用户。
足以说明快速响应的重要,然而app会更苛刻,因为用户关注点是唯一的,只有打开在使用的当前app。
同样的调查:
1. 60%用户希望启动时间少于2秒。
2. 79%的用户在遇到2次崩溃或卡死后会放弃使用。

用户–>内容(滚动,点击……)
内容–>用户(网络,布局,渲染……)

最重要的问题在于:

  1. 阻止交互(因为有事情做,没有马上响应用户操作)
  2. 等待(网络,绘制的耗时)
    这两个问题是独立的,但是很可能同时发生。开发者可以从这两点独立地优化他们。
主线程

因为iOS的SDK设计是UI必须在主线程,主要是为了让开发尽可能简单,相比单线程,多线程的代码往往会更复杂。还有一个原因是iOS设计初始单核的,那时认为多核的手机需要很久以后才会诞生。

什么拖慢了主线程?

大部分是准备工作:

  1. 布局(文本的测量,需要一层层计算)
  2. 渲染(文字,图片的绘制,解码工作)
  3. 系统对象(创建、销毁对象)
设计思路
Core

目标是避免主线程阻塞和能时熟悉UIKit的开发快速上手。
通过ASDisplayNode抽象就可以在子线程创建了。node扩展了UIKit,UIKit扩展了CoreAnimation!

在这里插入图片描述

当node创建的时候,背后是没有对象的,当.view或者.layer的属性被访问后,背后的对象才会被创建。
CoreAnimation会在主线程调用layout,后台计算的值会被应用。
每个节点的占位图会先展现,异步渲染是周期性的。

文本异步渲染的例子:

在这里插入图片描述

  1. 在子线程创建节点

在这里插入图片描述

  1. 在主线程添加视图

在这里插入图片描述

也可以直接添加view,添加完后会渲染node下的视图。

在这里插入图片描述

也可以把绘制移到子线程:

在这里插入图片描述

一些强大的特性

大致上有:优化(隐式的,无需写代码)、ASTextNode&ASImageNode、ASTableView&ASCollectionView、智能加载(优化网络请求顺序)、占位图。

并发:

  1. 异步会更流畅
  2. 并发减少等待时间
  3. iOS设备是多核的

主要的布局和渲染可以通过并发操作完成:

  1. view比layers更需要主线程。
  2. 很多继承元素不需要UIView的特性。
  3. Node可以通过isLayerBacked抽象。从node->view->layer到node->layer只需要设置node.layerBacked=YES

子树光栅化:
通过node.shouldRasterizeDescendants=YES设置。Texture的API以改为enableSubtreeRasterization方法。通过光栅化:

  1. 分开的图层更方便和灵活。
  2. 一个draw方法更高效。
  3. 跳过创建子views和layers。(CPU避免了views和layers绘制,GPU减少了压缩工作)

注意光栅化不适用于有alpha,缩放的图层,已经光栅化的node不能再添加新的node,以及当node的view或者layer已经加载后就不能再调用了。

二、框架结构

三、怎么使用

使用后对于要异步处理的UI我们不再使用’UIKit’,所以对于老项目改动成本较高。(对于降低使用成本可以考虑使用我后面发的’AsyncUI’)
因为万物皆节点,所以我们创建UI实际上是创建node。对于UI控件的node,基本遵照原UIKit的控件属性和方法,但也有一些改动,比如label的text属性移除了,只能使用attributeText:

#pragma mark ASDisplayNode
    ASDisplayNode *displayNode = [self childNode];
    
    parentNode = [self centeringParentNodeWithChild:displayNode];
    parentNode.entryTitle = @"ASDisplayNode";
    parentNode.entryDescription = @"ASDisplayNode is the main view abstraction over UIView and CALayer. It initializes and owns a UIView in the same way UIViews create and own their own backing CALayers.";
    [mutableNodesData addObject:parentNode];
    
#pragma mark ASButtonNode
    ASButtonNode *buttonNode = [ASButtonNode new];
    
    // Set title for button node with a given font or color. If you pass in nil for font or color the default system
    // font and black as color will be used
    [buttonNode setTitle:@"Button Title Normal" withFont:nil withColor:[UIColor blueColor] forState:UIControlStateNormal];
    [buttonNode setTitle:@"Button Title Highlighted" withFont:[UIFont systemFontOfSize:14] withColor:nil forState:UIControlStateHighlighted];
    [buttonNode addTarget:self action:@selector(buttonPressed:) forControlEvents:ASControlNodeEventTouchUpInside];
    
    parentNode = [self centeringParentNodeWithChild:buttonNode];
    parentNode.entryTitle = @"ASButtonNode";
    parentNode.entryDescription = @"ASButtonNode (a subclass of ASControlNode) supports simple buttons, with multiple states for a text label and an image with a few different layout options. Enables layerBacking for subnodes to significantly lighten main thread impact relative to UIButton (though async preparation is the bigger win).";
    [mutableNodesData addObject:parentNode];
    
#pragma mark ASTextNode
    ASTextNode *textNode = [ASTextNode new];
    textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum varius nisi quis mattis dignissim. Proin convallis odio nec ipsum molestie, in porta quam viverra. Fusce ornare dapibus velit, nec malesuada mauris pretium vitae. Etiam malesuada ligula magna."];
    
    parentNode = [self centeringParentNodeWithChild:textNode];
    parentNode.entryTitle = @"ASTextNode";
    parentNode.entryDescription = @"Like UITextView — built on TextKit with full-featured rich text support.";
    [mutableNodesData addObject:parentNode];

四、源码分析

ASDKViewController的初始化

使用__covariant修饰ASDKViewController:
__covariant
协变性,子类型可以强转到父类型(里氏替换原则),里氏替换值的是子类可以扩展父类的方法,但不应该复写父类的方法。作者让ASDKViewController完全继承ASDisplayNode的能力形成万物皆节点的世界。你可以使用ASDisplayNode的能力也可以无视,把ASDKViewController当普通的UIViewController使用也可以。

可以通过initWithNode:在初始化VC时指定一个node为VCNode,这里把一个tableNode作为VC的根node:

- (instancetype)init
{
  _tableNode = [ASTableNode new];
  
  self = [super initWithNode:_tableNode];
    
  return self;
}

initWithNode:里实际上调用了一个初始化方法:

- (instancetype)initWithNode:(ASDisplayNode *)node {
    ...
    _node = node;
    [self _initializeInstance];
}

_initializeInstance方法内判断了是否load,如果node没有初始化,会调用onDidLoad:的监听,然后再获取view:

- (void)_initializeInstance {
    ...
    // In case the node will get loaded
  if (_node.nodeLoaded) {
    // Node already loaded the view
    [self view];
  } else {
    // If the node didn't load yet add ourselves as on did load observer to load the view in case the node gets loaded
    // before the view controller
    __weak __typeof__(self) weakSelf = self;
    [_node onDidLoad:^(__kindof ASDisplayNode * _Nonnull node) {
      if ([weakSelf isViewLoaded] == NO) {
        [weakSelf view];
      }
    }];
  }
}

重写loadView方法,主要是为了替换原有VC的view,使他也具有node的特性:

  1. 苹果创建了一个frame和一个autoresizing masks,初始化一个view并不是很耗性能,所以我们用node替换这个临时的view获得了性能提升。
  2. 拿到苹果创建的view的frame和autoresizing masks,赋值给node,然后将self.view用node.view替换。
  3. - (void)propagateNewTraitCollection:(ASPrimitiveTraitCollection)traitCollection中通过一个自定义的UITraitCollection判断有无系统特性的修改,如果有,调用[_node layoutThatFits:[self nodeConstrainedSize]];来更新size。
- (void)loadView {
    ...
    UIView *view = self.view;
    CGRect frame = view.frame;
    UIViewAutoresizing autoresizingMask = view.autoresizingMask;

    // We have what we need, so now create and assign the view we actually want.
    view = _node.view;
    _node.frame = frame;
    _node.autoresizingMask = autoresizingMask;
    self.view = view;
    
    ...
    ASPrimitiveTraitCollection traitCollection = [self primitiveTraitCollectionForUITraitCollection:self.traitCollection];
    [self propagateNewTraitCollection:traitCollection];
}
- (void)propagateNewTraitCollection:(ASPrimitiveTraitCollection)traitCollection {
    ...
    if 有变化
    [_node layoutThatFits:[self nodeConstrainedSize]];
}

看看addSubnode:做了什么

addSubnode:在三个类有实现:

  1. ASDisplayNode
  2. UIView
  3. CALayer。

先看主线ASDisplayNode:
通过addSubnode:大致可以看出都做了哪些操作:

  1. 判断能否添加node。
  2. 加锁。
  3. 插入节点。
- (void)addSubnode:(ASDisplayNode *)subnode {
    ...
    1.判断
    ASDisplayNode *oldParent = subnode.supernode;
    if (!subnode || subnode == self || oldParent == self {
        return;
    }
    2.加锁 MutexLocker l(__instanceLock__);
    ...
    3.插入节点
    [self _insertSubnode:subnode atSubnodeIndex:subnodesIndex sublayerIndex:sublayersIndex andRemoveSubnode:nil];
}

关键在插入节点的方法中:

  1. 在节点添加、插入和替换时会调用。
  2. 这个方法和线程密切相关并且不持有锁。
- (void)_insertSubnode:(ASDisplayNode *)subnode atSubnodeIndex:(NSInteger)subnodeIndex sublayerIndex:(NSInteger)sublayerIndex andRemoveSubnode:(ASDisplayNode *)oldSubnode {
    1.一个宏定义,表示这个方法不能在node的view或者layer创建后的子线程调用。
    ASDisplayNodeAssertThreadAffinity(self);
    ...
    2.一些提前return的判断
        1)空的判断。
        2)不能添加view-backed的node到layer-backed的node上,这个很好理解,如果node只有layer,带view的node就不知道将view添加到哪了。
        3)如果node采用光刷化并且子node已经load过了直接return。因为当前node采用光栅化,那么添加到这个node的所有子node都会被合成到当前node的layer上,所以无需再添加了。
        4)subnode越界的判断:
        ```
        __instanceLock__.lock();
        NSUInteger subnodesCount = _subnodes.count;
        __instanceLock__.unlock();
        ```
        这里在取出nodeCount时加解锁,如果插入的index大于这个           count就直接return。
    ...
    3.添加到当前node的subnodes中
    [_subnodes insertObject:subnode atIndex:subnodeIndex];
    ...
    4.设置当前node的superNode
    [subnode _setSupernode:self];
    5.添加node的view或者layer
    如果是光栅化
    if (isRasterized) {
        进入继承链
        if (self.inHierarchy) {
          [subnode __enterHierarchy];
        }
    } else if (self.nodeLoaded) {
        插入node的view或者layer
        [self _insertSubnodeSubviewOrSublayer:subnode             atIndex:sublayerIndex];
    }
    
}

可以看出主要方法在__enterHierarchy_insertSubnodeSubviewOrSublayer:atIndex:里面。

先看一下如果是光栅化,[subnode __enterHierarchy];方法里做了什么事情:

  1. 保证主线程,因为会用到setNeedsDisplay这些主线程方法。
  2. 遍历所有node设置继承。
  3. 如果还没有contents,设置一个占位图。因为隐式动画会自动在下一个runloop提交,但是这里需要在当前runloop就提交,所以手动调用commit。
- (void)__enterHierarchy {
    1. 保证主线程,因为会用到setNeedsDisplay这些主线程方法。
    ASDisplayNodeAssertMainThread();
    ...
    
    2. 遍历所有node设置继承。
    [self willEnterHierarchy];
    for (ASDisplayNode *subnode in self.subnodes) {
        [subnode __enterHierarchy];
    }
    ...
    
    3. 如果还没有contents,设置一个占位图。
    提交一个transaction
    入栈
    [CATransaction begin];
    关闭隐式动画提高性能
    [CATransaction setDisableActions:YES];
    [self _locked_setupPlaceholderLayerIfNeeded];
    _placeholderLayer.opacity = 1.0;
    出栈
    [CATransaction commit];
    [layer addSublayer:_placeholderLayer];
}

如果不是光栅化,走插入node的方法:

  1. 因为是在主线程,可以免去一些锁的操作。
  2. 插入layer或者view。
- (void)_insertSubnodeSubviewOrSublayer:(ASDisplayNode *)subnode atIndex:(NSInteger)idx {
    ASDisplayNodeAssertMainThread();
    ...
    插入view或者layer
    if (canUseViewAPI(self, subnode)) {
        [_view insertSubview:subnode.view atIndex:idx];
    } else {
        [_layer insertSublayer:subnode.layer atIndex:(unsigned int)idx];
    }
}

设置光栅化后如何实现layers或者views的统一渲染

_ASDisplayLayer.mm中重写了display方法:

- (void)display {
    ...
    [self display:self.displaysAsynchronously];
}
- (void)display:(BOOL)asynchronously {
    ...
    [self.asyncDelegate displayAsyncLayer:self asynchronously:asynchronously];
}

通过代理去绘制内容:

- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously {
    ...
    1. 设置取消回调,如果是同步,不能取消。
    asdisplaynode_iscancelled_block_t isCancelledBlock = nil;
  if (asynchronously) {
    uint displaySentinelValue = ++_displaySentinel;
    __weak ASDisplayNode *weakSelf = self;
    isCancelledBlock = ^BOOL{
      __strong ASDisplayNode *self = weakSelf;
      return self == nil || (displaySentinelValue != self->_displaySentinel.load());
        };
    } else {
        isCancelledBlock = ^BOOL{
            return NO;
        };
    }
  
    2.设置display的block,这个block会返回一个绘制好的image
    asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO];
    
    3.设置完成的block
        asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
    4.设置layer的contents
    if (stretchable) {
        ASDisplayNodeSetResizableContents(layer, image);
      } else {
        layer.contentsScale = self.contentsScale;
        layer.contents = (id)image.CGImage;
      }
    };
    5.调用block,如果是异步,提交一个transation,同步就立即执行
    if (asynchronously) {
        _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
        
         [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
    } else {
        UIImage *contents = (UIImage *)displayBlock();
    completionBlock(contents, NO);
    }
}

node中在递归渲染的方法中主要是对subviews一层层渲染获得contents

- (void)_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock displayBlocks:(NSMutableArray *)displayBlocks {
    1.隐藏或透明的图层直接跳过。
    if (self.isHidden || self.alpha <= 0.0) {
      return;
    }
    2.如果父node没有设置光栅化,子node就不需要layout因为他们没有layers。
    if (rasterizingFromAscendent) {
      [self __layout];
    }
    3.如果是根节点,设置初始frame
    if (self.rasterizesSubtree) {
        frame = CGRectMake(0.0f, 0.0f, bounds.size.width, bounds.size.height);
    } else {
    4.否则计算新的frame
    }
    5.获得展示的block
    asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:NO isCancelledBlock:isCancelledBlock rasterizing:YES];
    if (shouldDisplay) {
    6.如果需要展示,设置一个block,内容是根据上下文和属性绘制image,并把这个block添加到displayBlocks中。
    [displayBlocks addObject:pushAndDisplayBlock];
    }
    7.递归操作
    for (ASDisplayNode *subnode in self.subnodes) {
        [subnode _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks];
    }
    if (shouldDisplay) {
    8. 添加一个pop的block,为了和上面add对应。并且设置成单例,因为都一样。
    dispatch_once(&onceToken, ^{
      popBlock = ^{
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextRestoreGState(context);
      };
    });
    [displayBlocks addObject:popBlock];
    }
}

MutexLocker l(instanceLock);

__instanceLock__是一个自己实现的递归锁AS::RecursiveMutex __instanceLock__;。为什么要自己实现,作者的解释是:Obj-C不允许你向C++的成员变量传递参数,这个锁实现了从默认非递归到递归的遍历。
猜测是为了封装复杂度,这个RecursiveMutex实际上是继承一个Mutex的类,在Mutex的类中有共用体:

Type _type;
union {
  os_unfair_lock _unfair;
  ASRecursiveUnfairLock _runfair;
  std::mutex _plain;
  std::recursive_mutex _recursive;
};

通过这个共用体区分锁的类型,在合适的场合可以用不同的锁。
其中ASRecursiveUnfairLock也是用unfairLock封装的:

typedef struct {
  os_unfair_lock _lock OS_UNFAIR_LOCK_AVAILABILITY;
  _Atomic(pthread_t) _thread;
  int _count;                  // Protected by lock
} ASRecursiveUnfairLock;

作者也指出(Recursive mutexes are a bad idea)递归锁要慎用,recursive-locks-will-kill-you
这篇文章指出递归锁的低效,你不关注这些多层加入的锁直接执行的任务耗时情况,这使得在一些场合变得很糟糕,所以要及时释放锁,关注你重复加锁的代码到底做了什么事情。

ASCellNode

@property BOOL neverShowPlaceholders;
默认是NO。
尽管有预加载,但当快速滑动时还是会来不及加载,为了时滑动不受阻力,采用一个默认图,一旦加载完毕,马上更换显示。
当这个设为YES,在没有加载完就滚到后主线程就会受阻,这使得表现和UIKit类似,但相比UIKit性能会更好。

@property (getter=isSelected) BOOL selected;
@property (getter=isHighlighted) BOOL highlighted;
同步设置有没有选中,加了锁。

@property (nullable) id nodeModel;
当前cell的view-model。
- (BOOL)canUpdateToNodeModel:(id)nodeModel;
对比模型和已有模型的class是否一致,即是不是可以用来更新的模型。

@property (nullable, nonatomic, readonly) UIViewController *viewController;
从主线程获取一个vc。

@property (nullable, weak, readonly) id<ASRangeManagingNode> owningNode;
拥有者,一般是ASTableNode 或者 ASCollectionNode。
通过- (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)node;可以获取indexPath。

如touchesBegan的手势操作。

五、我的扩展,AsyncUI

AsyncUI 是一个轻量级的UI异步提交库,启发来自于"Texture"和"YYDispatchQueuePool"。
相比于"Texture"更加轻量,没有重写或者hook原生 UIKit 的方法。因为"Texture"对于异步处理UI来说已经足够优秀,无需再重复造轮子,所以 AsyncUI 只是在 UIKit 的基础上做异步提交。特点是轻量,快速,低能耗。

它的主要功能如下:

  1. 异步UI任务处理提交将UI任务提交到主线程的runloop即将空闲阶段。
  2. 自己管理多个串行队列交替使用,来实现异步并发队列。
  3. 对不需要相应的多个UIView子视图合并到父视图,减少屏幕持有的图层。

开启1000个异步任务,线程数维持在最大16个:
在这里插入图片描述

合并图层,左没有合并,右合并到一个view上:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值