AsyncDisplayKit的核心组件包括:
· ASDisplayNode 与UIView对应 —一个子类,用来自定义node。
· ASControlNode. 类似于UIControl —用来扩展生成buttons。
· ASImageNode. 类似于UIImageView —异步的图像解码.
· ASTextNode. 类似于UITextView —基于TextKit构建,支持富文本的全部特性。
· ASTableView. UITableView子类,用于支持node。
1.基本单元是node。ASDisplayNode是uiview的抽象,也是CALayer的抽象。node不像view只能用在主线程,node是线程安全的。你可以在后台线程同时初始化和配置他们的整个继承链。
2.为了保持界面平滑和可相应,app应该保持每秒60帧的渲染,意味着主线程只有1/60秒来推每一帧。就是用16毫秒来执行所有布局和绘制代码。由于一些系统负载,你的代码经常只有10毫秒来运行,超过10毫秒就会掉帧。
3. AsyncDisplayKit让你把图片解码,文字缩放和渲染等昂贵的ui操作从主线程中去除。
优化
层处理 Layer-Backing
有些时候,大幅度使用Layer而不是使用views,可以提高你的app的性能,但是手动的把基于view开发的界面代码改为基于layer的界面代码,非常的费劲,如果有时候因为要开启触摸或者view特定的功能的时候,你可能要功能回退
当你使用ASDK的node的时候,如果你打算把所有的view转换成layer,只需要一行代码
rootNode.layerBacked = YES;
如果你想回退,也只需要删除这一行,我们建议不需要触摸处理的所有视图都开启
同步并发
敬请期待……
子树光栅化
预压缩,扁平化整个视图层级到一个图层,也可以提高性能,node也可以帮你做这件事
rootNode.shouldRasterizeDescendants = YES;
你的整个node层级都会渲染在一个layer下
AsyncDisplayKit的组件
节点容器
在容器中使用Nodes
AsyncDisplayKit有以下几种 Container
ASDK Node Container | UIKit Equivalent |
---|---|
ASCollectionNode | 代替UIKit的 UICollectionView |
ASPagerNode | 代替UIKit的 UIPageViewController |
ASTableNode | 代替UIKit的 UITableView |
ASViewController | 代替UIKit的 UIViewController |
ASNavigationControllerv | 代替UIKit的 UINavigationController ,遵循ASVisibility 协议 |
ASTabBarController | 代替UIKit的 UITabBarController ,遵循ASVisibility 协议 |
Node Container的优势
Node Container
可以自动地管理node
的智能预加载,它会保证node
的的布局计算、数据获取、解码和渲染会以异步的方式进行。
Node的子类
AsyncDisplayKit提供了以下几种node
。
相对UIKit的组件来说 node
最大的改进就是将所有布局和显示都放在子线程,因此主线程就能够即时地响应各种事件。
ASDK Node | UIKit Equivalent |
---|---|
ASDisplayNode | 代替UIKit的 UIView ,其他的node都继承于它 |
ASCellNode | 代替UIKit的 UITableViewCell 和 UICollectionViewCell ,用在 ASTableNode , ASCollectionNode 和 ASPagerNode 中 |
ASScrollNode | 代替UIKit的 UIScrollView ,这个 node 用在自定义滚动区域上非常有用 |
ASEditableTextNode | 代替UIKit的 UITextView |
ASTextNode | 代替UIKit的 UILabel |
ASImageNode | 代替UIKit的 UIImage |
ASNetworkImageNode | |
ASMultiplexImageNode | |
ASVideoNode | 代替UIKit的 AVPlayerLayer |
ASVideoPlayerNode | 代替UIKit的 UIMoviePlayer |
SControlNode | 代替UIKit的 UIControl |
ASButtonNode | 代替UIKit的 UIButton |
ASMapNode | 代替UIKit的 MKMapView |
AsyncDisplayKit 中的 node
相比UIKit中的组件更先进而且更方便。比如 ASNetworkImageNode
可以自动地加载和进行内存管理,而且还支持逐步加载Jpeg和动态gif图片。
node
的层级结构如下
上图中蓝色高亮的node是对UIKit元素的封装而成。比如 ASScrollNode
封装自 UIScrollView
, ASCollectionNode
封装自 UICollectionView
,ASMapNode
在实时地图模式中就是 UIMapView
封装。
基本重载方法
创建子类时,子类是
ASViewController
还是ASDisplayNode
有很大的区别。这感觉上是显而易见的,但是有的区别是很微妙的,所有也需要特别注意。
ASDisplayNode
- ASDisplayNode内部持有了UIView,将view封装起来
- ASDisplayNode充当UIView的delegate,原本view产生的各种事件,由于已经不直接操作UIView,因此会delegate通知node进行处理
- ASDiplayNode可以通过.view直接访问UIView
- ASDiplayNode管理着UIView,接管了UIView的一些处理操作
- ASDiplayNode通过对异步处理的改造,让使用者可以在安全的在线程进行操作
ASDisplayNode
如果你使用的 node
组件类型于 UIView
的子控件,就必须注意下面几条准则以确保你充分发挥了这个框架的潜力, node
也能很好的显示。
-init
当使用 nodeBlock
的时候这个方法是在子线程进行的。但是,如何 -init
没有运行完,其他方法就无法运行,所以这个方法永远不要上锁。
最重要的是你的 init
方法一定要能在任何队列中调用。尤其是不要在初始化的时候初始化任何 UIKit
控件、点击 node
的 view
或者 layer
或者添加任何手势。这些事情应该放在 -didLoad
中。
-didLoad
这个方法在概念上类似于 UIViewController
的 -viewDidLoad
方法,它代表着后台的 view
已经被加载完成了。要确保它是在主线程中执行的,在这个方法中很适合处理 UIKit
的各种操作(比如添加手势,点击 view
或者 layer
,初始化 UIKit
组件)。
-layout
在这个方法里调用 super
之后,布局规则对象会把所有的子节点都计算并且定位好,所以这个时间点是你手动进行布局所有子view的时机。或许更有用的是,有时候你想手动布局,但并不太容易创建一个布局规则对象,或者有时候你不想等所有子节点布局完毕,而只是很简单的手动设置frame,如果是这样的话,就在这个方法里写。
layoutSpecThatFits
这个方法就是用来建立布局规则对象,产生 node
大小以及所有子 node
大小的地方,你创建的布局规则对象一直持续到这个方法返回的时间点,经过了这个时间点后,它就不可变了。尤其重要要记住的一点事,千万不要缓存布局规则对象,当你以后需要他的时候,请重新创建。
ASViewController
ASViewController
是 UIViewController
的子类,包含了管理 node
的特性。因此它的所有方法一定要在主线程中使用。
-init
这个方法只调用一次,是在 ASViewController
的生命周期的最开始的时候调用。在它初始化的过程中,不要使用 self.view
或者 self.node.view
,它会强制 view
被提前创建。这些事情应该在 viewDidLoad
中完成。
ASViewController
的初始化方法是 initWithNode:
,代码如下所示。一个 ASViewController
管理节点就像 UIViewController
管理 view
一样,但是初始化的过程有小小的差异。
- (instancetype)init
{
_pagerNode = [[ASPagerNode alloc] init];
self = [super initWithNode:_pagerNode];
// setup any instance variables or properties here
if (self) {
_pagerNode.dataSource = self;
_pagerNode.delegate = self;
}
return self;
}
-loadView
建议不要使用这个方法,因为它相对于 viewDidLoad
来说没有明显的优势反而有很多不足的地方。但如果你不去设置 self.view
的属性就没什么问题。调用 [super loadView]
它就会执行 node.view
。
-viewDidLoad
它会在 ASViewController
的生命周期的最开始调用,仅次于 -loadView
。这是你使用节点的 view
最早的时机。在这个方法里适合放只执行一次并且要使用 view/layer
的代码,比如加手势。
布局的代码千万不要放在这个方法里,因为界面发生改变也不会再调用这个方法重新布局。
-viewWillLayoutSubviews
这个方法调用的时机和节点 -layout
方法调用的时机一样,它可以被执行很多次。只要它的节点被改变(比如旋转,分屏,显示键盘)或者继承()都会立即被调用。
- (instancetype)init
{
if (self = [superinitWithNode:[ASDisplayNodenew]]) {
[selfaddTableNode];
}
returnself;
}
- (void)addTableNode
{
_tableNode = [[ASTableNodealloc]initWithStyle:UITableViewStyleGrouped];
_tableNode.backgroundColor = [UIColorwhiteColor];
_tableNode.delegate =self;
_tableNode.dataSource =self;
[self.nodeaddSubnode:_tableNode];
}
/
- (void)viewDidLoad {
[superviewDidLoad];
// Do any additional setup after loading the view.
// 在一个控制器中读取数据,作为_tableNode的数据源
[selfloadData];
}
- (void)loadData
{
[selfloadMoreDataWithContext:nil];
}
// 读取数据的方法,context作为数据的上下文
- (void)loadMoreDataWithContext:(ASBatchContext *)context
{
if (context) {
[context beginBatchFetching];
}else {
_curIndexPage =1;
_haveMore =YES;
[LPLoadingViewshowLoadingInView:self.view];
}
LPHttpRequest *hotZoneRequest = [LPGameZoneOperation requestHotZoneWithPageIndex:_curIndexPage];
[hotZoneRequest loadWithSuccessBlock:^(LPHttpRequest *request) {
LPHotZoneModel *hotZoneModel = request.responseObject.data;
NSArray *threadList = hotZoneModel.threadList;
if (context) {
if (threadList.count >0) {
NSMutableArray *indexPaths = [NSMutableArray array];
for (NSInteger row =_threadList.count; row< _threadList.count+threadList.count; ++row) {
[indexPaths addObject:[NSIndexPathindexPathForRow:rowinSection:0]];
}
_threadList = [_threadList arrayByAddingObjectsFromArray:threadList];
[_tableNode.view insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
_curIndexPage++;
_haveMore =YES;
}else {
_haveMore =NO;
}
[context completeBatchFetching:YES];
}else {
_focusList = hotZoneModel.focusList;
_forumList = hotZoneModel.forumList;
_threadList = hotZoneModel.threadList;
[selfaddCycleScrollView];
[_tableNode.viewreloadData];
++_curIndexPage;
_haveMore =YES;
[LPLoadingView hideLoadingForView:self.view];
}
} failureBlock:^(id<TYRequestProtocol> request,NSError *error) {
[LPLoadingView hideLoadingForView:self.view];
if (context) {
[context completeBatchFetching:YES];
}else {
__weaktypeof(self) weakSelf =self;
[LPLoadFailedView showLoadFailedInView:self.view retryHandle:^{
[weakSelf loadData];
}];
}
}];
}
用批抓取进行无限滚动
在大部分 App 中,服务器上拥有的数据远远多于在表格中一次能显示下的 cell 的行数。也就是说,每个 App 都应采用某些机制,来保证用户浏览到当前数据集结尾时,随时从服务器上拉取新的数据。
过去,这只能通过 Scroll View 的委托方法 -scrollViewDidScroll: 来进行手动处理。在 ASDK 中,有一种更明确的解决方式。
你可以预先指定多少页,才需要加载新的数据。
首先,取消被注释的助手方法。找到 AnimalTableController.m 最后,取消 Helpers 类别中的两个方法注释。-retrieveNextPageWithCompletion: 方法可以看出是网络调用,而 -insertNewRowsInTableNode: 方法只是一个一般方法,将新的数据添加到表格中。
首先,在 -viewDidLoad: 方法中加入一句:
- 1
- 1
将 leadingScreensForBatching 设置为 1.0 表示你当用户滚动还剩 1 个全屏就到达数据末尾时,就开始抓取新的一批数据。
然后,在 Delegate 类别中加入这个方法:
- 1
- 2
- 3
- 1
- 2
- 3
这个方法告诉表格,在这次批抓取之后是否还可以进行新的批抓取。如果你知道 API 上的数据什么时候结束,返回 NO,表示不用进行新的批抓取了。
因为你想让表格无限滚动,所以返回 YES,表示永远能够进行新的批抓取。
然后,继续加入:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
这个方法在用户即将滚动到表格末尾并且 -shouldBatchFetchForTableNode: 方法返回 YES 时调用。
代码解释如下:
- 首先,请求一次新批抓取,以备显示。通常这会从 API 返回一个对象数组。
- 在完成块中,用新数据刷新表格。
- 最后,调用 -completeBatchFetching: ,传递一个 YES 表示你已经完成。在本次批抓取完成之前,不会进行新的批抓取。
@property (nonatomic,strong)ASNetworkImageNode *imageNode;
ASNetworkImageNode
作用同等于 UIImageView
,如果使用网络图片请使用此类,ASDK
用的是第三方的图片加载库PINRemoteImage,ASNetworkImageNode
其实并不支持 gif,如果需要显示 gif 推荐使用FLAnimatedImage。
ASDK的容器
每一个Node节点是可以直接通过UIView的addSubnode
方法添加到原本的UIView之上,但是官方在文档里并不推荐这么做,我前一篇的官方文档翻译中有写
当在项目中替换使用AsyncDisplayKit的时候,一个经常犯的错误就是把一个Node节点直接添加到一个现有的view视图层次结构里。这样做会导致你的节点在渲染的时候会闪烁一下
相反,你应该你应该把nodes节点,当做一个子节点添加到一个容器类里。这些容器类负责告诉所包含的节点,他们现在都是什么状态,以便于尽可能有效的加载数据与渲染。你可以把这些类当做UIKit和ASDK的整合点
借助nodes容器可以更好的对容器内子nodes进行管理和渲染控制,这是官方推荐使用的,因此我们引出了节点容器的概念
ASViewController
ASViewController就是这样的一种节点容器,它并非继承自ASDisplayNode,而是直接继承自UIViewController,就好像每一个UIViewController一定要有一个self.view一样,ASViewController必须由一个ASDisplayNode进行初始化。
ASTableNode
ASCollectionNode
这两个节点,本身就是一个ASDisplayNode节点,但它同时也有节点容器的作用
不仅如此,Table和Collection都是用于滚动并且批量展示数据的,ASDK还特意为此封装了一套复杂的处理----滚动情况下的数据异步加载过程。
因此他内部有两个很特殊的控制器
- ASRangeController 用于智能判断滚动范围和滚动方向,提起对即将滚入屏幕的区域进行预处理控制,包括预处理数据加载,和预处理渲染
- ASDataController 专门用于数据相关的加载控制,在RangeController的指挥下,对指定区域内的数据进行加载。
- (instancetype)initWithItem:(LPZoneDiscuzItem *)item
{
if (self = [superinit]) {
_item = item;
self.backgroundColor = [UIColorwhiteColor];
[selfaddImageNode];
[selfaddTitleNode];
[selfaddDetailTextNode];
}
returnself;
}
在ASDK 2.0版本中,.preferredFrameSize
被改为width
、height
等其他一系列属性,详见最新版头文件
对象指定.flexShrink
,.flexGrow
则是表示,该对象所处在的LayoutSpec位置不足/多余时,其是否会自动缩小/扩充。
ASDK 2.0之前,.flexShrink
,.flexGrow
默认为NO
,2.0之后,将默认打开
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
_imageNode.preferredFrameSize =CGSizeMake(54,54);
_titleNode.flexShrink =YES;
_detailTextNode.flexShrink =YES;
ASStackLayoutSpec *verStackLayout = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVerticalspacing:10justifyContent:ASStackLayoutJustifyContentStartalignItems:ASStackLayoutAlignItemsStretchchildren:@[_titleNode,_detailTextNode]];
ASStackLayoutSpec *horStackLayout = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontalspacing:10justifyContent:ASStackLayoutJustifyContentStartalignItems:ASStackLayoutAlignItemsCenterchildren:@[_imageNode,verStackLayout]];
ASInsetLayoutSpec *insetLayout = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(10,13, 10,10) child:horStackLayout];
return insetLayout;
}
- 最后通过一个
ASInsetLayoutSpec
设置一个边距。
ASStackLayoutSpec
可以说这是最常用的类,而且相对于其他类来说在功能上是最接近于 AutoLayout
的。 之所以称之为盒子布局是因为它和 CSS 中 Flexbox
很相似,不清楚 Flexbox
的可以先看下阮一峰的这篇博客。
先看一个例子:
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
self.childNodeA.style.preferredSize = CGSizeMake(100, 100);
self.childNodeB.style.preferredSize = CGSizeMake(200, 200);
ASStackLayoutSpec *stackLayout = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
spacing:12
justifyContent:ASStackLayoutJustifyContentStart
alignItems:ASStackLayoutAlignItemsStart
children:@[self.childNodeA, self.childNodeB]];
return stackLayout;
}
简单的说明下各个参数的作用:
-
direction
:主轴的方向,有两个可选值:- 纵向:
ASStackLayoutDirectionVertical
- 横向:
ASStackLayoutDirectionHorizontal
- 纵向:
-
spacing
: 主轴上视图排列的间距,比如有四个视图,那么它们之间的存在三个间距值都应该是spacing
-
justifyContent
: 主轴上的排列方式,有五个可选值:-
ASStackLayoutJustifyContentStart
从前往后排列 -
ASStackLayoutJustifyContentCenter
居中排列 -
ASStackLayoutJustifyContentEnd
从后往前排列 -
ASStackLayoutJustifyContentSpaceBetween
间隔排列,两端无间隔 -
ASStackLayoutJustifyContentSpaceAround
间隔排列,两端有间隔
-
-
alignItems
: 交叉轴上的排列方式,有五个可选值:-
ASStackLayoutAlignItemsStart
从前往后排列 -
ASStackLayoutAlignItemsEnd
从后往前排列 -
ASStackLayoutAlignItemsCenter
居中排列 -
ASStackLayoutAlignItemsStretch
拉伸排列 -
ASStackLayoutAlignItemsBaselineFirst
以第一个文字元素基线排列(主轴是横向才可用) -
ASStackLayoutAlignItemsBaselineLast
以最后一个文字元素基线排列(主轴是横向才可用)
-
-
children
: 包含的视图。数组内元素顺序同样代表着布局时排列的顺序,所以需要注意
主轴的方向设置尤为重要,如果主轴设置的是 ASStackLayoutDirectionVertical
, 那么 justifyContent
各个参数的意义就是:
-
ASStackLayoutJustifyContentStart
从上往下排列 -
ASStackLayoutJustifyContentCenter
居中排列 -
ASStackLayoutJustifyContentEnd
从下往上排列 -
ASStackLayoutJustifyContentSpaceBetween
间隔排列,两端无间隔 -
ASStackLayoutJustifyContentSpaceAround
间隔排列,两端有间隔
alignItems
就是:
-
ASStackLayoutAlignItemsStart
从左往右排列 -
ASStackLayoutAlignItemsEnd
从右往左排列 -
ASStackLayoutAlignItemsCenter
居中排列 -
ASStackLayoutAlignItemsStretch
拉伸排列 -
ASStackLayoutAlignItemsBaselineFirst
无效 -
ASStackLayoutAlignItemsBaselineLast
无效