AsyncDisplayKit 是一个 源自 Facebook 的 Paper 项目的UI 框架。它完美解决了 Paper 项目团队面临的众多核心问题之一:如何使主线程尽量清晰明了。
现在,许多 App的用户体验都严重依赖于连续的手势和物理动画。至少,你的 UI 界面可能依赖于一些滑动视图。
这种类型的用户接口大多完全取决于主线程并且对主线程阻塞异常敏感。一旦主线程阻塞就意味着动画掉帧并产生一个极差的用户体验。
主线程中要做的主要工作如下:
- 尺寸和布局:一些像
-heightForRowAtIndexPath:
或者调用 UILabel 的-sizeThatFits
方法,也包括AutoLayout
的约束求解器的指数级开销。 - 图像解码: 在一个图像框中使用
UIImage
就意味着要先将图像数据解码。 - 绘图:复杂的文本也需要手动完成绘图渐变和阴影。
- 对象生存期:创建、使用、销毁系统对象(例如创建一个
UIView
)。
在正确使用时,AsyncDisplayKit就可以默认异步执行所有尺寸,布局和渲染操作。并且无需额外优化,App 主线程需要完成的工作量也会显著减少。
除了性能上的出色表现,新版的 AsyncDisplayKit 还提供了仅仅使用几行简单的代码就可以完成复杂功能并且方便调用的编程接口。
这篇 AsyncDisplayKit 2.0教程由两部分组成,你可以从中学到使用 ASDK完成一个易用动态应用程序的全部技能。在第一部分,你会学到构建一个 App的想法构思。在第二部分,你会学到如何创建自己的node
子类和如何使用ASDK 的强有力布局引擎。完成这段教程需要 Xcode7.3 并且需要熟悉 Objective-C。
免责声明: ASDK 和自动视图布局是不兼容的,所以在教程中将不会使用。
尽管 ASDK 完全支持 Swift,许多使用者始终使用 Objective-C。
现在最流行的前100个 App 并没有包含任何 Swift(其中至少6个使用了 ASDK)。
因为以上原因,这个教程使用 Objctive-C。
1.开始
在开始阶段,马上去下载示例教程
这个示例教程使用了CocoaPods来使用 ASyncDisplayKit。所以,在使用 Cocoapods 时,打开 RainforestStarter.xcworkapace 而不是 RainforestStarter.xcodeproj。
注意:使用教程过程中需要网络连接。
构建运行程序,这个 App 是由包含一系列动物的UITableView。如果你在 AnimalTableController 里查看相关代码,你会发现是一个平时大家都会经常见到的 UITableViewController 类。
注意:请确保实在真机运行实例程序而不是在模拟器中。
滑动这些动物并且注意掉帧情况。你不需要使用instruments
来检查就可以看出这款 APP 需要优化来提高性能。
你可以使用 AsyncDisplayKit 来解决这个问题。
2.介绍ASDisplayNode
ASDisplayNode 是 ASDK 中的核心类,仅仅是 MVC中“view”对象,就像UIView
和CALayer
。认知 node 最好的方式是类比你已经熟悉的 UIViews 和 CALayers 之间的关系。
需要记住的是一款 App 中显示在屏幕上的一切都是通过 CALayer 对象表示出来的。UIView 创建并且拥有一个底层的 CALayer,并且在 CALayer 上添加触摸手势和其他功能。UIView 并不是 CALayer 的子类,相反的它们扩展了 CALayer 的功能。
这层抽象在ASDisplayNode 中得到扩展:你可以认为Node包裹一个视图,就像视图包裹一个层。
Node 能够对普通视图做的优化是默认的创建并配置一个后台队列并且并发进行渲染。
幸运的是,关于Node
的 API 和 UIView 或者 CALayer 的API惊人的相似。视图中常用的许多属性在node
类中同样适用。你甚至可以到达底层的视图或者图层。
3. 节点容器
尽管节点自身提供了巨大性能提升的可能,而与如下容器类协同使用时才更容易发生性能提升。
这些类包括:
- ASViewController:一个 UIViewController 子类可以提供管理节点。
- ASCollectionNode 和 ASTableNode:Node 相当于 UICollectionView 和 UITableView,其子类在底层维持。
-
ASPagerNode: 相比于 UIKit 中的 UIPageViewController,ASCollectionNode 子类提供了更加优异性能。
说的不错,但是真正的提示来自 ASRangeController 任何其中的类都影响这容器内的节点。
4. 转化 TableView
第一件要做的事就是转换现在的 tableView 到一个 tableNode。
替换 tableView为 tableNode
首先,转到 AnimalTableController.m。添加代码到其他引用之下。
#import <AsyncDisplayKit/AsyncDisplayKit.h>
这句引用了 ASDK 来使用该框架。然后继续替换其中的 tableView属性:
@property(strong,nonatomic)UITableView * tableView;
使用接下来的 tableNode:
@property(strong,nonatomic)ASTableNode *tableNode;
这会造成类中许多代码需要更改,但是不用担心。
不用担心,这些错误和警告会指导你从现在有的到你想要的。
这些错误在-viewDidLoad
中的,事实上 tableView 已经不存在了。如果你打算将代码中所有的 tableView 换为 tableNode 应该做如下的操作:
- 声明一个 ASTableNode 作为属性。
- 一个 tableNode 没有一个叫
-registerClass:forCellReuseIdentifier:
。 - 无法添加一个Node 作为子视图。
现在你应该替换-viewDidLoad
用以下代码。
- (void)viewDidLoad{
[super viewDidLoad];
[self.view addSubnode:self.tableNode];
[self applyStyle];
}
有趣的是你在对 UIView 调用了-addSubnode:
。这个方法是通过 UIView 的类目实现的,和一下代码相同:
[self.view addSubview:self.tableNode.view];
下一步,使用以下代码替换方法定义来修正-viewWillLayoutSubviews
。
- (void)viewWillLayoutSubviews{
[super viewWillLayoutSubviews];
self.tableNode.frame = self.view.bounds;
}
所有做的这些都是用self.tableNode
替换self.tableView
来设置表视图的尺寸。接下来,找到-applyStyle
方法并用如下代码替换:
-(void)applyStyle{
self.view.backgroundColor = [UIColor blackColor];
self.tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone;
}
这行设置了表视图的分隔样式的代码是唯一一行改变的代码。注意 tableNode 的视图属性为了设置表视图的分隔样式怎么实现的。ASTableNode 并没有暴露全部 UITableView 的属性,所以你必须使用 tableNode 的底层 UITableView 实例来改变 UITableView 特定属性。
现在,添加如下代码到-initWithAnimals
的开始处。
_tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain];
并且添加如下代码到末位,在初始化方法返回之前:
[self wireDelegation];
这个初始化方法使用 tableNode 初始化 AnimalTableController 并且调用-wireDelegation
来使tableNode 的代理生效。
设置 tableNode 的数据源和代理
就像 UITableView,ASTableNode 使用一个数据源和代理来获取关于自身的信息。TableNode 的 ASTableDataSource 和 ASTableDelegate 协议是和 UITableViewDataSource 和 UITableViewDelegate 相似的。事实上,它们定义了一些几乎相同的方法就像:-tableNode:numberOfRowsInsection:
。这两个系列的协议并不是完美的匹配的因为 ASTableNode 表现的和 UITableView 略有不同。
找到'-willDelegation'并且在方法实现中用 tableNode 替换 tableView。
- (void)wireDelegation{
self.tableNode.dataSource = self;
self.tableNode.delegate = self;
}
现在,你发现 AnimalTableController 不在遵守正确的协议了。一般来说,AnimalTableController 遵守 UITableViewDataSource 和 UITableViewDelegate。在接下来的部分为了是 tableviewNode 正确运行,你需要设法遵守并实现每个协议。
遵守 ASTableDataSource
对于上面提到的 AnimalTableController.m ,找出如下的数据源类目接口声明。
@interface AnimalTableController (Delegate)<UITableViewDelegate>
@end
并且用ASTableDelegate 替换 UITableViewDelegate。
@interface AnimalTableController (Delegate)<ASTableDelegate>
@end
现在 AnimalTableController 遵守 ASTableDelegate,是时候处理具体实现了,导航到 AnimalTableController.m 底部找到代理类目的实现。
我很确定你已经意识到需要提供一个方法类似-tableView:heightForRowAtIndexPath:
。这是因为在 UIKit 中,每个格子的高度是通过代理计算并返回的。
ASTableDelegate 缺少 -tableView:heightForRowAtIndexPath:
。在 ASDK 中,所有的 Node 都会自行计算自己的尺寸。并不是提供一个固定的高度,你可以随意指定最小和最大尺寸来定义格子大小。这次,你需要指定每个格子的高度至少是屏幕的三分之二。
现在无需关心太多,在文章第二部分会详细说明:
现在,用以下代码来替换-tableview:heightForRowAtIndexPath:
- (ASSizeRange)tableView:(ASTableView *)tableNode constrainedSizeForRowAtIndexPath:(NSIndexPath *)indexPath{
CGFloat width = [UIScreen mainScreen].bounds.size.width;
CGSize min = CGSizeMake(width,([UIScreen mainScreen].bounds.size.height/3)*2);
CGSize max = CGSizeMake(width,INFINITY);
return ASSizeRangeMake(min,max);
}
在作完以上更改之后,构建并运行查看效果。
现在是一个滑动顺滑的表格视图了。接下来准备好进行下一步优化。
5. 使用批次获取实现无限滑动
在大多数 App 中,服务器的数据比平均要显示在屏幕上的个数要多的多。这意味着我们需要在用户接近当前数据集合结束时,分批次的从服务器获取数据;
许多时候,这个都是人工观察滑动视图的偏移量来调用代理方法-scrollViewDidScroll:
。在 ASDK 中,有更多的方法来做这件事,例如你可以提前说明提前加载内容的页数。
要做的第一件事情是取消方法中的注释,找到 AnimalTableController.m 并且取消 Helpers 类目中的注释。你可以认为-retrieveNextPageWithCompletion:
是网络请求,并且-insertNewRowsInTableNode:
是一个非常标准的向表中添加元素的方法。
下一步,添加如下代码到 -viewDidLoad:
。
self.tableNode.view.leadingScreensForBatching = 1.0;
将 leadingScreensForBatching 设置为 1.0意味着你希望不论何时只要滑动到了距离底部只有一屏幕内容的时候,就要获取新的内容了。
接下来,添加如下方法到代理的实现中。
- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode{
return YES;
}
这个方法是用来决定表视图此后是否应该保持继续请求数据,如果你知道已经到达了数据末尾,就在该方法返回 NO 来阻止产生新的数据请求。
如果你打算表视图一直滑动,就返回 YES。
接下来,添加如下方法:
- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context{
[self retrieveNextPageWithCompletion:^(NSArray *animals){
[self insertNewRowsInTableNode:animals];
[context completeBatchFetching:YES];
}];
}
当用户滑动接近表视图尽头这个方法会被调用,并且当表视图已经从-shouldBatchFetchForTableNode:
接受了 YES。
回顾一下这部分都做了什么:
1. 首先,我们请求了需要显示的下一批数据。通常这是一个从服务器返回的对象数组。
2.在结束时,使用新下载的数据更新视图。
3.最后,确保在完成是使用 YES 调用 -completeBatchFetching:
。新的数据获取不会被调用除非当前请求已经完成。
现在构建运行程序,开始滑动发现是无限滑动的视图。
6.智能预加载
在你编写过得 APP 中,不乏一些需要在使用滑动视图而需要提前加载内容的需求。也许你在做一个全屏幕的画廊应用,并且经常需要提前加载一些图片来让用户滑动时尽量不再看到占位图。
当你做这样的工作时,你发现有很多问题需要思考:
- 要占用多少内存
- 需要提前加载多少内容
-
打算什么时候决定使用资源来反馈用户操作。
并且在你思考多维度内容的时候这就变得愈发复杂。如果你有一个页视图控制器,而每个控制器中又有一个集合视图,现在你需要思考如何在两个方向上实现动态加载。
还记得我告诉你把 ASRangeController 放在角落里,现在开始考虑使用它。
在每个容器类中都有一个概念来标记每个 Node 的视图状态。在任何时间,一个 Node 都是由一下组合而成:
- Preload Range:通常是距离可见部分最远的距离。这是当格子中的每个子节点内容需要从外部资源加载的时候;一个编程接口或者本地缓存。这和需要格子自行获取对象模型的获取形成对比。
- Display Range:这里,进行了图片解码图形渲染等显示任务。
-
Visible Range:在这里整个节点都是可见的。即使只有一个像素。
以上的范围也可以使用“屏幕大小”来度量,也可以很容易的装换为 ASRangeTuningParameters 属性。
例如,当你正在使用一个 ASNetworkImageNode 类来在每个页中显示图像。每个节点在进入预加载范围是都会从网络请求数据,并且在进入显示区域时解码图片。
事实上,如果你不想搞得太清楚,也不必把这些想的太难。通过组件化,在默认情况下,就可以充分利用这些优势并得到显著收益。
注意: 有一件不太明显的事情就是这些区域并不会堆叠。相反,这些区域会在可见区域部分重叠覆盖。如果你同时设置显示和预加载在屏幕上,它们会恰好同时开始。需要显示的数据需要尽可能的加载,所以预加载区域需要大一点,使节点在开始显示程序时已经准备好了进入可视区域。
一般来说,头部的区域比尾部的要大一些。当用户改变滑动方向时,反向的区域尺寸在用户滑动时也会依次实现。
7. 节点界面状态回调
你也许想知道这些区域到底是怎么工作的,我很高兴你会问到。
每个系统中的节点都有一个interfaceState
属性,这个属性是一个“比特区域”类型是 ASInterfaceState。当 ASCellNode 移动通过一个类型是ASRangeController
的滑动视图,每个子节点都会相应的更新它们的 interfaceState
属性。这意味着即使是树中最深的节点也会响应 interfaceState
改变。
幸运的是,很少需要直接处理节点的interfaceState
属性。更多的是你仅仅想要对一个节点改变到或形成一个特定的状态做出反应。这是节点界面状态回调的用处。
命名节点
为了观察一个节点移动的状态,需要给节点取一个名字。这样你就可以观察到每个节点加载数据,显示内容,渲染到屏幕并且之后在相反方向做这些事情。
回到 -tableNode:nodeBlockForRowAtIndexPath:
,找到这样的注释:
//You'll add something extra here later...
在它下面,添加下面这一行给每一个格子一个调试名字。
cardNode.debugName = [NSString stringWithFormat:@"cell %zd",indexPath.row];
现在你可以追踪格子的通过区域的过程。
观察格子
进入 CardNode_InterfaceCallbacks.m。这里你可以使用发现六个方法来追踪一个节点通过某个区域的过程。取消注释,构建并运行。确保 Xcode 中控制台可见,接下来慢慢滑动。观察滑动时不同格子的状态改变。
注意:大多数情况下,唯一你需要关心的 ASInterface 改变状态的方法是 `-didEnterVisibleState`或者 `-didExitVisibleState`。这么说来,许多工作都说是在底层替你完成的。为了充分了解整合了预加载和显示状态我们可以做些什么,现在去看一看`ASNetworkImageNode`中的代码。你甚至都不用抬一下手指,所有的网络图片都会自动获取并解码它们的内容,并释放内存。
智能预加载 2
在第二个发布版本中,智能预加载的概念被扩充为多个方向 。如果你有一个垂直滑动的表视图,并且在某一位置屏幕上包含一个横向滑动的集合视图。
尽管这个集合现在技术上是在可见区域,你不会想要提前加载整个集合视图。相反,每个滑动视图都有自己的 ASRangeController 来管理配置区域改变的参数。
进入第二个维度
现在你已经完成了 AnimalTableController,你可以在一个 ASPagerNode 中作为一个页面使用它。
你会用来包含这个页面的控制器已经存在在项目中了,所以你需要做的第一件事是进入到 AppDelegate.m
找到 -installRootViewController
并将:
AnimalTableController *VC= [[AnimalTableController alloc] initWithAnimals:[RainforestCardInfo allAnimals]];
替换为:
AnimalTableController *vc = [[AnimalTableController alloc]init];
之后,进入 AnimalTableController.m 并添加如下代码到初始化方法返回语句之前。所有你需要做的就是创造一个新的页面并且设置他的数据源到这个控制器。
_pagerNode = [[ASPager alloc]init];
_pagerNode.dataSource = self;
页面节点事实上是 ASCollectionNode 的子节点 预先配置的使用方法和 UIpageViewController 一致。好消息是这个的 API 比 UIPageViewController 要简单一些。
接下来你要做的是实现数据源方法。进入 ASPagerDataSource 类目中底部的实现内容。
首先,实现页面数量
- (NSInteger)numberOfPagerNode:(ASPagerNode *)pagerNode{
return self.animals.count;
}
之后,你需要实现 -pagerNode:nodeAtIndex:
,类似于你之前在 ASTableNode 中实现的节点代码块数据源方法。
- (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeIndex:(NSInteger)index{
CGSize pagerNodeSize = pagerNode.bounds.size;
NSArray *animals = self.animals[index];
ASCellNode *node = [[ASCellNode alloc] initWithViewControllerBlock:^{
return [[AnimalTableController alloc] initWithAnimals:animals];
} didLoadBlock:nil];
return node;
}
让我们看看这一部分都做了些什么:
- 尽管这个版本并不是基于 BLOCK,首先获取数据模型是很好的练习。
- 这次,你使用了强有力的初始化方法:
-initWithViewControllerBlock:
。所有你需要做的是返回一个返回表视图节点控制器的 Block。
一旦你添加这个方法,你会有一个完全功能的页面,页面的格子是由你之前创建的表节点控制器控制的。