欢迎来到 IT实训基地-南通科迅教育
咨询电话:0513-81107100
iOS开源库源码解析之AsnycDispalyKit
2016/11/22
科迅教育
321
南通网页设计培训学校怎么挑选-基础教学

前言

最近心血来潮,想研究下FaceBook的AsnycDispalyKit的源代码,学习一些界面优化的技术以及编码风格。这篇文章,会详细的记录下我认为对新手有用的部分。后面有空的时候,继续研究其他几个iOS开发很流行的库-AFNetworking,SDWebImage,MBProgressHud,Mantle等`。AsnycDisplayKit是一个非常庞大的库,所以我尽量捞干的讲。

关于AsyncDisplayKit

文档 源代码

如果只是想优化界面,那么可以用AsyncDisplayKit来重写哪些性能要求比较高的部分


界面顿卡的原因

iOS的屏幕是60fps,也就是说,每一帧的间隔是1/60s,大概16.7ms。
每一帧显示需要三步

CPU计算好视图(UIView)的大小,位置,对图片进行解码,绘制好纹理交给GPU GPU根据纹理,顶点进行空间变换,渲染后放到帧缓冲区 每当帧信号到达的时候,从帧缓冲区取一帧,显示到屏幕上

也就是说,整个CPU+GPU处理的时间是16.7ms,如果超过这个时间,那么当前绘制的一帧就没办法放到帧缓冲区,帧信号到达的时候,取的还是上一帧的数据。也就是造成了界面没有变化,显示顿卡。也就是说,为了解决顿卡,一般要从CPU和GPU两个角度来考虑

CPU限制

对象的创建,释放,属性调整。这里尤其要提一下属性调整,CALayer的属性调整的时候是会创建隐式动画的,是比较损耗性能的。 视图和文本的布局计算,AutoLayout的布局计算都是在主线程上的,所以占用CPU时间也很多 。 文本渲染,诸如UILabel和UITextview都是在主线程渲染的 图片的解码,这里要提到的是,UIImage只有在交给GPU之前的一瞬间,CPU才会对其解码。

GPU限制

视图的混合,比如一个界面十几层的视图叠加到一起,GPU不得不计算每个像素点药显示的像素 视图的Mask,比如圆角什么的,会触发离屏渲染,占用GPU时间。 半透明,GPU不得不进行数学计算,如果是不透明的,CPU只需要取上层的就可以了 浮点数像素

AsnycDisplayKit通过很多技巧来解决这些问题,后文我会一点点分析如何实现的。


AsyncDisplayKit是啥

这是Facebook推出的一个框架,用在Paper的App中。用来保证复杂的界面交互的时候,也不会掉帧。
通过名字就可以看出来,AsyncDisplay就是异步加载控件。了解UIKit的同学都知道,UIKit的中的UIView和CALayer的布局和渲染都是在主线程上进行的,当界面复杂的时候,也就会占用大量时间导致掉帧。这个框架是建立在UIKit之上的,对UIView进行了进一步的封装-Node。Node支持异步的绘制UIView。Asnyc有一个原则

能放到后台执行的代码就尽量放到后台,不能放到后台执行的代码就尽量优化(比如用Runloop对任务进行拆分)

ASDealloc2MainObject

这个类中,AsnycDisplayKit重新定义了Release和Reatin方法来让一个类支持自己引用计数,可以强制的让对象在主线程dealloc,不过这个文件是MRC的,也就是要在build setting中添加-fno-objc-arc。

那么,为什么要强制的在主线程dealloc呢?因为UIKit的对象不是线程安全的,只能在主线程上进行dealloc

Tips:

1.可以在文件中,添加如下代码

?
1
2
3
<codeclass="hljs cs">#if__has_feature(objc_arc)
#error This file must be compiled without ARC. Use -fno-objc-arc.
#endif</code>

来让编译器检查本文件只能在MRC条件下编译。
关于如何重写Release和Reatian,可以在这个文件里找到_AS-objc-internal.h

2.由于Define只是在编译期进行简单替换,可以通过#defeine的方式为类条件添加代码


大量的断言和宏定义

通过阅读源代码可以发现,代码中使用了大量的宏和断言

?
1
2
3
4
5
<codeclass="hljs erlang">- (void)dealloc
{
  ASDisplayNodeAssertMainThread();
  //Other codes
}</code>

其中ASDisplayNodeAssertMainThread()为宏定义,

?
1
2
3
<codeclass="hljs smalltalk">  #define ASDisplayNodeAssertWithSignal(condition, description, ...) NSAssert(condition, description, ##__VA_ARGS__)
 #define ASDisplayNodeAssertMainThread() ASDisplayNodeAssertWithSignal([NSThread isMainThread], nil, @"This method must be called on the main thread")
</code>

对于断言和宏定义使用很少的同学,可以看看这个头文件,会对你很有帮助。

Tips:
合理的使用断言NSAssert,能够让你的代码在更早的地方出现问题,方便发现问题进行调试。在XCode 7中,NSAssert默认只会在Debug模式下起作用,在release模式下不会起作用,


Objective C++

在AsyncDisplay中,可以看到很多.mm后缀的文件,例如

Objective C++和Objective C类似,它的文件组成由一个.h和一个.mm组成,你可以使用C++的语法,例如命名空间,由于编译过后会被链接到OC runtime,所以,也可以使用OC的类。

简单来说,利用Objective C++,你可以使用C++和OC来混合编程


clang

新手开发往往忽略了开发中很重要的一个环节-编译,iOS开发的编译器是-clangllvm。通过编译器,往往可以

修改编译器的警告模式,开启全部警告 忽略某一个编译器的警告 条件编译代码,和if else很像

所以,如果读到这里,还是对iOS的编译环节没有一个清楚的认识,建议看看clang和LLVM的wiki。另外,研究下XCode中build settings下所有的内容,还有,我之前的这篇博客也介绍了Clang的警告相关

举例看看这个库中是如何使用的

1.pragma once,让一个文件在一个单独的编译中只包含一次,类似ifndef…define…endif或者import

2.在看看处理警告

?
1
2
3
4
5
6
<codeclass="hljs cs">#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wdeprecated-declarations"
  if([_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNodeForItemAtIndexPath:)]) {
    [_asyncDelegate collectionView:self didEndDisplayingNodeForItemAtIndexPath:indexPath];
  }
#pragma clang diagnostic pop</code>

这简单提一下,编译的过程会有一个类似编译状态的堆栈,先压栈保存当前的状态,然后忽略-Wdeprecated-declarations这个警告,在编译完这段代码之后,再出栈恢复之前的状态

3.开启所有的警告 -Wall


ASDisplayNode

这里不得不提到了,UIView和CALayer的关系


所有视图可见的部分,本质上都是CALayer显示的 CALayer不能接受触摸,UIView相当于是CALayer的代理,用来接受触摸,响应responser,以及发出各种通知。 在性能要求较高的地方,往往使用Layer,因为Layer更加轻量级

AsnycDisplayKit仿照这种代理关系,对UIView进行了进一步的封装-ASDisplayNode。其中,Node是可以异步绘制的,也可以是layerBased


ASDisplayNode的继承方式如下


可以看到,类似于UIView与UILabel的关系,ASDisplayNode是AsyncDisplayKit中可视部分node的基类。


异步渲染

displaysAsynchronously;

这个属性用来决定是否是异步绘制的,异步绘制的流程如下

当一个View被添加到View的层次结构中,needsDisplay 会返回true 在布局结束后,Core Animation会调用_ASDisplayLayer的display方法,在displayQueue队列上添加一个绘制任务s 当绘制实际执行的时候,会调用代理方法-drawRect:或者-display 一个绘制任务会被添加到asyncdisplaykit_async_transaction中,绘制都结束后,回调完成block

简单来讲将绘制封装成任务,提交到displayQueue(后台队列)执行

我们来看下,AsyncDisPlayKit异步渲染的一段代码_ASDisplayLayer.h

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<codeclass="hljs objectivec">- (void)_hackResetNeedsDisplay
{
  ASDisplayNodeAssertMainThread();//断言在主线程
  // Don't listen to our subclasses crazy ideas about setContents by going through super
  super.contents =super.contents;//设置当前的contents为super.conents
}
 
- (void)display
{
  [self _hackResetNeedsDisplay];//先在主线程上设置当前的contents为super.contents
 
  ASDisplayNodeAssertMainThread();//保证在主线程
  if(self.isDisplaySuspended) {//检查display是否被挂起,如果被挂起则返回,不需要展示
    return;
  }
 
  [self display:self.displaysAsynchronously];//根据displaysAsynchronously属性来判断是否需要异步展示
}
 
- (void)display:(BOOL)asynchronously//绘制任务交给代理-代理设计模式
{
  [self _performBlockWithAsyncDelegate:^(id<_ASDisplayLayerDelegate> asyncDelegate) {
    [asyncDelegate displayAsyncLayer:self asynchronously:asynchronously];
  }];
}</code>

然后我们再看看代理是如何异步绘制的,代码在这个文件ASDisplayNode+AsyncDisplay.mm,这个方法 - (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously
代码较长,我挑出核心部分来讲解,其中display的核心就是这个

?
1
2
3
4
5
6
<codeclass="hljs objectivec">//这个block用来进行实际的绘制
 asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO];
 
//省略中间代码
[transaction addOperationWithBlock:displayBlock queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
</code>

而addOperationWithBlock,就是把绘制任务,添加到后台GCD任务组里

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<codeclass="hljs erlang">- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block
                        queue:(dispatch_queue_t)queue
                   completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion
{
  ASDisplayNodeAssertMainThread();
  ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions");
 
  [self _ensureTransactionData];
 
  ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion];
  [_operations addObject:operation];
  dispatch_group_async(_group, queue, ^{
    @autoreleasepool{
      if(_state != ASAsyncTransactionStateCanceled) {
        operation.value = block();
      }
    }
  });
}</code>

layerBacked

对于,那些不需要接受触摸,不需要响应UIView的各种通知的Node,可以把layerBacked设置为true。这样,AsyncDisplayKit会自动使用Layer作为Node的backed。上文也提到了Layer相对于UIView的各种优点,这里不再赘述


SubTree预合成

shouldRasterizeDescendants来决定,所谓预合成,就是几个Layer合并成一个Layer来处理

这个属性,用来决定是否需要将子Node都绘制到当前context中,也就是预合成.

预合成的优点

占用更少的内存,因为子Layer不需要单独被创建出来 CPU不需要再为子Layer计算布局,大小 GPU在渲染的时候,只需要绘制一层纹理,不需要进行混合

预合成的代码,可以参考ASDisplayNode+AsyncDisplay.mm中的_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock这个方法,当预合成开启的时候,会递归的检查subnode,绘制到当前ImageContext中。关闭的时候,每一个subnode维护自己的Context


预加载

所谓预加载,就是在视图尚未出现在屏幕上的时候,进行网络请求,布局计算,图片解码,后台视图渲染。然后,随着屏幕滚动,预先加载的内容就不需要再占用时间进行计算了,直接拿过来显示就可以了。

根据预加载的程度,AsyncDisplayKit把显示区域划分成几个类型

Visible Range,在屏幕上的区域 Display Range,被认为是将要展示的区域,这个区域会进行布局计算,图片解码,后台渲染等预加载 Fetch Data Range,进行预先数据获取的区域(网络或者磁盘)


关于预加载,可以参考ASTableView的代码,
Range的定义

?
1
2
3
4
5
6
<codeclass="hljs objectivec">typedef NS_ENUM(NSInteger, ASLayoutRangeType) {
  ASLayoutRangeTypeDisplay,
  ASLayoutRangeTypeFetchData,
  ASLayoutRangeTypeCount
};
</code>

AsyncDispaly主要用三个类实现预加载

ASDataController 用来在后台处理数据 ASRangeController,用在ASTableview和ASCollectionView中,管理区域的变化。 ASFlowLayoutController,用来获取当前屏幕上处在各个区域的cell的indexPath

这里面的代码逻辑和架构设计都十分复杂,感兴趣的同学可以研究下源代码。
这里,我列出来ASTableivew的初始化代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
<codeclass="hljs avrasm">  _layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:ASFlowLayoutDirectionVertical];
 
  _rangeController = [ASDisplayNode shouldUseNewRenderingRange] ? [[ASRangeControllerBeta alloc] init]
                                                                : [[ASRangeControllerStable alloc] init];
  _rangeController.layoutController = _layoutController;
  _rangeController.dataSource = self;
  _rangeController.delegate = self;
 
  _dataController = [[dataControllerClass alloc] initWithAsyncDataFetching:NO];
  _dataController.dataSource = self;
  _dataController.delegate = _rangeController;
 
  _layoutController.dataSource = _dataController</code>

看起来是不是很有趣呢?

_rangeController持有_layoutController的一个引用 _layoutController的数据源是_dataController _dataController的代理又是_rangeController

其中,_layoutController的核心是这个方法 - 根据rangeType来获取对应的indexSet

?
1
2
<codeclass="hljs erlang">- (NSSet *)indexPathsForScrolling:(ASScrollDirection)scrollDirection rangeType:(ASLayoutRangeType)rangeType
</code>

_dataController的数据源是self(ASTableview),也就是它依赖ASTableivew为自己提供数据,然后Delegate是_rangeController,也就是说,可以这么理解_dataController作为一个桥梁,由ASTableview提供数据,并且提供接口给ASTableview调用,回调给_rangeController。

通过阅读ASDataController的接口可以看到,这个类主要就是用来处理数据的,处理ASTable的插入删除reload等,比如

?
1
2
3
4
5
6
<codeclass="hljs erlang">- (void)insertRowsAtIndexPaths:(NSArray<nsindexpath> *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
 
- (void)deleteRowsAtIndexPaths:(NSArray<nsindexpath> *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
 
- (void)reloadRowsAtIndexPaths:(NSArray<nsindexpath> *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
</nsindexpath></nsindexpath></nsindexpath></code>

_rangeController的dataSrouce和delegate都是self,也就是它是直接和ASTableview打交道的,而代理和数据源如下图,通过名字就可以看出来,rangeController用来实际的管理区域的变化。并且回调给ASTableview

这么设计的最大的优点是什么? - 解耦,让不同的类分别处理某一块逻辑,这样代码的逻辑清楚,方便测试,方便后期维护


布局计算

通常开发iOS的时候,布局有两种

AutoLayout 手动计算位置大小(layoutSubViews,或者viewDidLayoutSubviews里调整位置)

AutoLayout有一个明显的缺点:Layout计算在主线程,并且随着视图量级的增加,AutoLayout占用的时间成指数级别上升。手动调整布局虽然性能上好一些,但是缺需要大量的计算。

AsyncDisplay定义了自己的布局引擎,采用了asp">CSS Box模型。CSS的box模型布局更加灵活。

那么,AsyncDisplayKit如何实现了Box的模型呢?

主要由协议ASLayoutAble来定义一个node是可以Layout,

?
1
2
3
<codeclass="hljs objectivec">@protocolASLayoutable
//包括相对于前一个node的距离,后一个的距离,自己的position等信息,用来计算位置
@end</asstacklayoutable,></code>

Runtime的使用

这个术语老生常谈的了,Runtime在很多开源库,包括很多项目开发的时候都会用到,并不是什么黑科技。只是利用好OC的语言特性罢了。比如ASInternalHelpers.mm里利用Runtime去检查子类是否重写了一个Selector

?
1
2
3
4
5
6
7
8
9
<codeclass="hljs php">BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector)
{
  Method superclassMethod = class_getInstanceMethod(superclass, selector);
  Method subclassMethod = class_getInstanceMethod(subclass, selector);
  IMP superclassIMP = superclassMethod ? method_getImplementation(superclassMethod) : NULL;
  IMP subclassIMP = subclassMethod ? method_getImplementation(subclassMethod) : NULL;
  return(superclassIMP != subclassIMP);
}
</code>

又比如,ASBasicImageDownloader.mm利用Runtime动态为类NSURLRequest“添加“属性

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<codeclass="hljs objectivec">@interfaceNSURLRequest (ASBasicImageDownloader)
@property(nonatomic, strong) ASBasicImageDownloaderContext *asyncdisplaykit_context;
@end
 
@implementationNSURLRequest (ASBasicImageDownloader)
staticconstchar*kContextKey = NSStringFromClass(ASBasicImageDownloaderContext.class).UTF8String;
- (void)setAsyncdisplaykit_context:(ASBasicImageDownloaderContext *)asyncdisplaykit_context
{
  objc_setAssociatedObject(self, kContextKey, asyncdisplaykit_context, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (ASBasicImageDownloader *)asyncdisplaykit_context
{
  returnobjc_getAssociatedObject(self, kContextKey);
}
@end</code>

Runloop的使用

问:Runloop和Runtime有啥关系?

答:没啥关系。

Runloop是一个和线程相关的技术,Runloop和线程一一绑定的。App启动的时候,默认启动一个main runloop来接收iOS系统的触摸,各种事件,响应端口来处理各种硬件相关。底层的Runloop实现就是一个for循环,不断的接受处理任务,没有任务的时候进行休眠。

通常,Runloop的使用场景有四个

 

监听Main Runloop的状态,在主线程空闲的时候或者合适的时候去执行一些任务。 把任务拆分成一个个小任务,依次提交到Runloop里执行。而不是,一个大任务去执行。大任务的执行会导致占用线程时间过多,导致之前所说的掉帧。 创建一个后台等待执行任务的线程,并且开启Runloop来等待任务。 根据Runloop的Mode来提交不同的任务,然后根据状态在Mode之前切换

 

Cocoa Touch的很多技术都和Runloop相关,比如CATransaction,NSTimer,AutoReleasePool等,对了,像这样的代码

?
1
2
3
<codeclass="hljs objectivec">dispatch_async(dispatch_get_main_queue(), ^ {
    [self presentViewController:vc animated:YES completion:nil];
});</code>

是会推迟到下一个Runloop执行的。

AsyncDisplayKit中,是如何使用Runloop的呢?关于Runloop的核心使用代码,都可以在这个文件里找到
_ASAsyncTransactionGroup.m,它使用Runloop的模式是我上文提到的场景1,也就是监听MainRunloop的状态,在适当的时候,去更新UI。

对于Runloop理解比较差的同学,可以看看我之前写的这篇文章《 iOS SDK详解之Runloop

监听Main Runloop的状态,

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<codeclass="hljs objectivec">+ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup
{
  ASDisplayNodeAssertMainThread();
  staticCFRunLoopObserverRef observer;
  ASDisplayNodeAssert(observer == NULL, @"A _ASAsyncTransactionGroup should not be registered on the main runloop twice");
  // defer the commit of the transaction so we can add more during the current runloop iteration
  CFRunLoopRef runLoop = CFRunLoopGetCurrent();
  CFOptionFlags activities = (kCFRunLoopBeforeWaiting |// before the run loop starts sleeping
                              kCFRunLoopExit);         // before exiting a runloop run
  CFRunLoopObserverContext context = {
    0,          // version
    (__bridgevoid*)transactionGroup, // info
    &CFRetain,  // retain
    &CFRelease, // release
    NULL        // copyDescription
  };
 
  observer = CFRunLoopObserverCreate(NULL,       // allocator
                                     activities, // activities
                                     YES,        // repeats
                                     INT_MAX,    // order after CA transaction commits
                                     &_transactionGroupRunLoopObserverCallback, // callback
                                     &context);  // context
  CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
  CFRelease(observer);
}
</code>

当调用这个方法的时候,每当Runloop进入kCFRunLoopBeforeWaiting或者kCFRunLoopExit的时候,_transactionGroupRunLoopObserverCallback回调就会执行。


Tips:之所以监听kCFRunLoopBeforeWaiting和kCFRunLoopExit是因为正常的UIKit就是监听Main Runloop,然后选择在Runloop这个时候进行渲染和界面的更新。AsyncDisplayKit同样监听这两个状态,但是优先级更低,所以会等到UIKit渲染结束了然后自己才会渲染。

在来看看这个回调

?
1
2
3
4
5
6
<codeclass="hljs cs">staticvoid_transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity,void*info)
{
  ASDisplayNodeCAssertMainThread();
  _ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info;
  [group commit];
}</code>

可以看到,在监听到这两个状态的时候,AsyncDisplaykit调用了commit,来更新当前的状态转换组。也就是根据Runloop的状态来找到适合更新UI的机会-主线程相对空闲的时候。


总结

ASDisplay接近200个源文件,实在太大了,本文也只是管中窥豹,简单的记录下我的理解,而且它还在更新。对了,对于大多数开发者来说,目标是开发稳定流畅的App,所以不要重新造轮子。

后面有时间继续看开源代码写博客吧,React Native的博客写起来也好慢,慢慢来吧

77
关闭
先学习,后交费申请表
每期5位名额
在线咨询
免费电话
QQ联系
先学习,后交费
TOP
您好,您想咨询哪门课程呢?
关于我们
机构简介
官方资讯
地理位置
联系我们
0513-91107100
周一至周六     8:30-21:00
微信扫我送教程
手机端访问
南通科迅教育信息咨询有限公司     苏ICP备15009282号     联系地址:江苏省南通市人民中路23-6号新亚大厦三楼             法律顾问:江苏瑞慈律师事务所     Copyright 2008-