AsyncDisplayKit 2.0 Tutorial: Getting Started
前言
"艺术是你任何能做到极致的事"
AsyncDisplayKit 是一个UI框架,最初诞生于 Facebook 的 Paper 应用程序。它是为了解决 Paper 团队面临的核心问题之一:如何尽可能缓解主线程的压力?
现在,许多应用程序的用户体验,很大程度上依赖于持续手势和物理动画。至少,你的UI可能是依赖于某种形式的 scrollView
。
这些类型的用户界面完全依赖于主线程,并且对主线程阻塞非常敏感。主线程阻塞将导致丢帧,降低用户的体验。
一些主线程开销较大的任务包括:
-
计算尺寸和布局:比如
-heightForRowAtIndexPath:
,或者在UILbel中调用-sizeThatFits
以及指数上升的AutoLayout‘s
布局计算。 -
图像解码:想要在一个 image view 中使用
UIImage
,首先要进行解码。 - 绘图:复杂的文本以及手动绘制渐变和阴影。
- 对象生命周期:创建,操纵和销毁系统对象(即创建一个UIView)
当正确使用时,AsyncDisplayKit 允许您在默认情况下异步执行所有测量、布局和渲染。无需任何额外的优化,一个应用程序可以减少约一个数量级的主线程开销。
除了这些性能优势,酷炫的 AsyncDisplayKit 还为开发者提供的便利接口,用简洁的代码就能完成复杂的功能。
在这两部分 AsyncDisplayKit 2.0 教程中,你将掌握使用ASDK构建一个实用的和动态的应用程序的所有要素。在第一部分中,你将要学习一些在你构建应用程序时可以用到的宏观思想。在第二部分中,你将学习如何构建自己 node 的 subclass,以及如何使用ASDK强大的布局引擎。为了更好的完成本教程,你需要会使用 Xcode 以及 熟悉 Objective-C。
免责声明:ASDK不兼容 Interface Builder和AutoLayout,因此,您将不会在本教程中使用它们,虽然ASDK完全支持Swift(除了ComponentKit),许多开发者仍在使用
Objective-C。免费App排行榜前100大多数都没有使用Swift(至少6个使用ASDK)。出于这些原因,本系列将重点介绍 Objective-C。话虽这么说,我们已经包括了一个Swift版本的实例项目。(嘴上说没有,代码还是很诚实的😂~)
开始
首先,下载初始项目。
该项目使用 CocoaPods 来拉入AsyncDisplayKit。所以,在正常的 CocoaPods 体系下,打开 RainforestStarter.xcworkspace
而不是RainforestStarter.xcodeproj
。
注意:需要网络连接才能完成本教程。
构建并运行以查看包含 UITableView
动物列表的应用程序。如果你看过了代码,AnimalTableController
你会发现这是一个正常且熟悉的 UITableViewController
类。
注意:确保在真机上运行本教程中的代码,而不是在模拟器中运行。
向上滑动你将看到帧数丢失引起的卡顿。你不需要启动控制台,以便能发现到这个应用程序需要在性能方面上的一些优化。
你可以通过 AsyncDisplayKit 的力量来解决这个问题
ASDisplayNode 简介
ASDisplayNode
是ASDK的核心类,它只是一个类似于 MVC 中的 “View” 一样的UIView
或 CALayer
。认识一个 node 的最佳方法是参照你已经熟悉的 UIViews
和 CALayers
之间的关系。
记住,iOS应用程序中的所有在屏幕上的显示都通过CALayer
对象表示的。UIViews
创建并且拥有一个底层的 CALayer
,并为他们添加触摸处理和其他交互功能。UIView
并不是 CALayer
的子类,而是相互环绕,扩展其功能。
这种抽象的情况下扩展 ASDisplayNode
:您可以将它们视为包装一个 view,就像在 view 上添加一个 layer 一样。
通常由 Node 创建的一个常规的view,其创建和配置都在行队列中执行,并且异步渲染。
幸运的是,用于处理 Node 的 API 对于任何使用过的 UIViews
或者 CALayers
的人来说应该异常的熟悉。所有 View 的属性都可以等效为 Node 类。你可以访问基础的 view 或者 layer 本身,就像是访问 view.layer
一样
节点容器(The Node Containers)
虽然 Node 本身提供了巨大的性能改进的可能,但真正的强大的是它们与四个容器类结合使用时产生的黑魔法。
这些类包括:
-
ASViewController:一个
UIViewController
的子类,允许你提供要管理的 Node。 -
ASCollectionNode and ASTableNode:Node 等效于
UICollectionView
和UITableView
,其子类实际上保留在底层。 -
ASPagerNode:一个
ASCollectionNode
的子类,提供极好的滑动性能相比与UIKit
的UIPageViewController
来说。
说得好,但真正的黑魔法来自 ASRangeController
这些类用于影响所包含的 Node 的行为。现在,跟着我并把你们的脑袋放空吧~
TableNode
你要做的第一件事就是将当前 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
已经被替换掉。我不会让你通过 tableNode
替换 所有的 tableView
实例(我的意思是,查找和替换并非那么难),但是如果你做了,你会看到:
- 你应该为
ASTableNode
分配一个属性。 - table Node 没有调用
-registerClass:forCellReuseIdentifier:
方法。 - 你不能添加一个 node 到 subview
此时,你应该将 -viewDidLoad
中的方法替换为:
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubnode:self.tableNode];
[self applyStyle];
}
这里要注意一个有趣的情况,你调用的是 UIView 的一个 -addSubnode:
方法,该方法是通过 category 添加到 UIView
上的,等效于:
[self.view addSubview:self.tableNode.view];
接下来,修改 -viewWillLayoutSubviews
中的代码:
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
self.tableNode.frame = self.view.bounds;
}
这样就替换用 self.tableNode
替换了 self.tableView
,并且设置了 table 的 Frame
继续修改 -applyStyle
方法中的代码为:
- (void)applyStyle {
self.view.backgroundColor = [UIColor blackColor];
self.tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone;
}
这是唯一设置 table 的 separatorStyle
的一行代码。注意 tableNode 的 view 是如何访问 table 的 separatorStyle
属性的。ASTableNode
不会暴露所有UITableView
的的属性,所以你必须通过 tableNode 底层的 UITableView
实例去设置 UITableView
的特殊属性。
然后,在 -initWithAnimals:
方法中添加。
_tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain];
并且在 return 之前,调用:
[self wireDelegation];
这就会在初始化 AnimalTableController
的时候,创建了一个 tableNode 并且调用 -wireDelegation
方法 设置 tableNode 的 代理。
设置 TableNode 的 DataSource & Delegate
类似于 UITableView
,ASTableNode
也使用 DataSource 和 Delegate 来设置本身。TableNode 的ASTableDataSource
和 ASTableDelegate
protocols 非常类似于 UITableViewDataSource
和 UITableViewDelegate
。
事实上,虽然他们定义了一些完全相同的方法,如 -tableNode:numberOfRowsInSection:
,但两组协议也不完全相同,因为 ASTableNode
行为和UITableView
还以所有不同的。
找到 -wireDelegation
方法, 并用 tableNode
替换 tableView
:
- (void)wireDelegation {
self.tableNode.dataSource = self;
self.tableNode.delegate = self;
}
现在, 你会收到警告, AnimalTableController
实际上不符合协议。目前,AnimalTableController
仅遵循 UITableViewDataSource
和 UITableViewDelegate
协议。在下面的章节中,我们将遵循这些协议,使我们能够使用 tableNode 的功能。
遵循 ASTableDataSource
在 AnimalTableController.m
开头的地方找到 AnimalTableController
的 DataSource
扩展声明:
@interface AnimalTableController (DataSource)<UITableViewDataSource>
@end
用 ASTableDataSource
替换 UITableViewDataSource
为:
@interface AnimalTableController (DataSource)<ASTableDataSource>
@end
现在,AnimalTableController
已经遵循了 AnimalTableController
协议。本就该如此了。
导航到 AnimalTableController.m
的底部并找到 DataSource
category 的实现。
首先,将 UITableViewDataSource
的 -tableView:numberOfRowsInSection:
方法,
更改为ASTableDataSource
的版本。
- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section {
return self.animals.count;
}
接着,ASTableNodes
的 cells 会以不同于 UITableView
的方式返回。用下面的代码替换 -tableView:cellForRowAtIndexPath:
以适应新的规则。
// 1
- (ASCellNodeBlock)tableNode:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath {
// 2
RainforestCardInfo *animal = self.animals[indexPath.row];
// 3 return ASCellNodeBlock
return ^{
// 4
CardNode *cardNode = [[CardNode alloc] initWithAnimal:animal];
//You'll add something extra here later...
return cardNode;
};
}
让我们整理一下:
- ASDK 中的
ASCellNode
等价于UITableViewCell
或者UICollectionViewCell
。要注意的是这个方法返回的是一个ASCellNodeBlock
,ASTableNode
维持着内部所有的 Cell,每个 indexPath 对应一个 block,并且随时准备进行初始化。 - 你的首要任务是通过数据模型构建cell。这是非常重要的一步,要注意!你获取数据后在 下面的 block 处理。不要在 block 里引用
indexPath
,以防止 block 运行前的数据变动。 - 然后返回一个 block,其返回值必须为
ASCellNode
。 - 没有必要担心Cell的复用以及初始化一个Cell的方法。您可能会注意到您现在返回了
CardNode
,而不是CardCell
。
这让我想到一个重要的点。或许你已经了解到,使用 ASDK 不需要复用 cell,好吧,我已经说了两遍了,但能记住就好。请随意删除顶部kCellReuseIdentifier
的定义吧
static NSString *kCellReuseIdentifier = @"CellReuseIdentifier";
你不必再担心 -prepareForReuse
了
遵循 ASTableDelegate
在 AnimalTableController.m
顶部,找到以下Delegate类别接口声明:
@interface AnimalTableController (Delegate)<UITableViewDelegate>
@end
用 ASTableDelegate
替换 UITableViewDelegate
:
@interface AnimalTableController (Delegate)<ASTableDelegate>
@end
现在 AnimalTableController
已经遵循了 ASTableDelegate
,是时候做处理了。在 AnimalTableController.m
底部找到 Delegate
分类的实现。
我们都知道,每个 UITableView
至少都要提供一个 -tableView:heightForRowAtIndexPath:
实现方法,因为每个 cell 的高度都由代理计算和返回。
ASTableDelegate
中没有 -tableView:heightForRowAtIndexPath:
。再 ASDK 中,所有的 ASCellNode
都负责确定自己的大小。你可以选择为单元格定义最小和最大尺寸,而不是提供静态高度。这种情况下,你希望每个cell的高度至少为屏幕的 2/3。
现在不用担心太多,这个会在第二部分中介绍。
现在只需要替换 -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);
}
经过我们的辛勤劳动,重新编译、运行项目,看看发生了什么。
真是一个流畅的 tableView
!一旦你开始做了,那就让我们做的更好吧!
无限滚动
在大多数应用中,服务器的数据点的个数往往会多于当前 tableView 中显示的单元格数量。这意味着,你必须通过某些手段做无缝处理,以便用户刷完当前数据列表时从服务端加载新的数据。
很多时候,这是通过手动观察滚动视图方法中的内容偏移来处理 scrollViewDidScroll:
, 使用 ASDK, 有一种更具说明性的处理方式。相反的,你可以预先确定好你需要加载的页数。
你要做的第一件事是取消已经存在的方法的注释。在 AnimalTableController.m
的结尾,取消 Helpers
分类中的两个方法。你可以认为 -retrieveNextPageWithCompletion:
是你的网络调用,而 -insertNewRowsInTableNode:
是个非常典型的再表中添加新的元素的方法。
接下来,在 -viewDidLoad
添加:
self.tableNode.view.leadingScreensForBatching = 1.0; // overriding default of 2.0
设置 leadingScreensForBatching
为 1.0 意味着当用户滑动一个屏的时候,就会载入新的数据。
继续,在 Delegate
分类中实现:
- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode {
return YES;
}
该方法用于告诉 tableView
是否继续请求新的数据。如果返回 NO
,则在到达 API 数据末尾时,不会再不会发出任何请求。
因为你希望无限滚动,那就返回 YES
,以确保总是请求新的数据。
接下来,还要添加:
- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context {
//1
[self retrieveNextPageWithCompletion:^(NSArray *animals) {
//2
[self insertNewRowsInTableNode:animals];
//3
[context completeBatchFetching:YES];
}];
}
该方法在用户滑动到 table 的末端并,且 -shouldBatchFetchForTableNode:
方法返回 YES
时被调用。
让我们回顾下上面的章节:
- 首先,你要请求新的 animals 数据来展示。通常是通过 API 来获取的一组array。
- 完成后,用新下载的数据更新 tableView
- 最后,确保
-completeBatchFetching:
返回的是YES
,即大功告成。在完成操作之前,不会进行新的数据请求。
Build and Run,并且不停的滚呀滚。你将会看到不停的看到一只鸟,他们是无限的。
智能预加载
你在工作中是否曾经遇到需要预先加载内容到 scrollView 或者 pageView 控制器中?也许你正在处理一个充满屏幕 image ,并且总是希望在接下来的几张图片加载时处于等待状态,所以用户很少看到占位符。
当你再这样的体系下工作时,你很快就会意识到有很多问题要考虑。
- 你占用了多少内存
- 你应该提前多久加载内容
- 你决定什么时候忽略用户的交互反映
并且当你考虑到多个维度的内容时,将些问题将会变得更加复杂。假设你有一个pageViewController
,里面每个 viewController
都带有一个 collectionView
。现在,你就需要考虑如何在两个方向上动态加载内容。同时,还要对每个设备进行优化。
还记得告诉你 ASRangeController
是不重要的吗?现在,这将是我们的重点。
在每个容器类中,所有包含的 node 都有一个接口状态的概念。在任何给定的时间,一个 node 可以是下面的任意组合:
- Preload Range(预载范围):通常最远的范围从可见区域。这是当cell的每个 subNode (例如ASNetworkImageNode) 的内容从外源加载,例如API和本地缓存。这与批量获取时,使用用模型对象代表cell本身形成对比。
- Display Range(显示范围):在这里进行显示任务,例如文本绘制和进行图像解码。
- Visible Range(可见范围):此时,node 至少有一个像素在屏幕上。
这些范围也适用于 screenfuls 的度量,并且可以使用 ASRangeTuningParameters
属性轻松调整。
例如:你正在使用一个 ASNetworkImageNode
在 gallery 的每个页面中展示图像,当每个cell进入 Preload Range 时,会发送网络请求,并且在进入 Display Range 时进行图像解码。
通常来说,你不必对这些 Ranges 太较真。利用好已有的组件,如:ASNetworkImageNode
和 ASTextNode
,通常来说你将会获得极大的便利。
注意: 有件不明显的事,这些 Ranges 不是堆栈的。相反,它们会在
Visible Range 上重叠和汇聚。如果将显示和预取都设置为一个屏幕,则它们将完全相同。通常数据需要存在才能显示,所以一般预取范围应该稍大一点。那么在 node 到达该范围时,就可以开始显示。
通常,该范围的前侧大于后侧。当用户改变其滚动方向时,范围的大小也是相反的,以便于对应用户实际移动的方向。
Node接口的状态回调
你可能会疑惑:这些 Ranges 是如何正确工作的?很高兴你这样问~
系统中的每个 node 都有一个interfaceState
属性,是一个带有字段((NS_OPTION)ASInterfaceState
类型。ASRangeController
负责管理 ASCellNode
在 scrolView
上的移动,每个subNode 都由一个 interfaceState
属性做对应的更新。这意味着即使时 tree 中最深的 nodes 也可以相应 interfaceState
的变化。
幸运的是,我们很少需要直接去操作 node 的 interfaceState
上的 二进制位。更常见的做法时,你只需要对某 node 的特定的状态进行更改。这就是接口的状态回调。
Node 命名
为了看到一个 node 的各种状态,给它命名时很有必要的。这样,你就可以监测每个 node 的数据加载、内容成、屏幕展示以及所以的事情。
回到代码-tableNode:nodeBlockForRowAtIndexPath:
,添加一句注释
//You'll add something extra here later...
在它的下面,给 cardNode
添加一个 debugName
:
cardNode.debugName = [NSString stringWithFormat:@"cell %zd", indexPath.row];
观察 Cells
进入 CardNode_InterfaceCallbacks.m
中,你可以找到六种追踪 node 在 ranges 中的状态的方法。取消注释,Build and Run。打开你的控制台,然后慢慢滑动 table。对照你的滑动,观察cell在对应的状态变化。
注意: 大多数情况下,你只要关心
-didEnterVisibleState
或-didExitVisibleState
方法对ASInterfaceState
的改变。或者说,已经为你做好了许多引擎。你可以查看ASNetworkImageNode
中的代码,看看你集成的通过Preload
和Display
状态实现的功能。 所有 node 网络图片的请求和解码,以及内存的释放都是自动完成,不费吹灰之力。
智能预加载(续)
在 2.0 版本中,已经介绍了多个维度上智能与加载的概念。假设你有一个竖直滚动的tableView
,在其中某些Cell包含了水平滚动的 collectionView
。
尽管现在的技术能够实现,但你不会希望在到达可见区域之前预先加载全部的 collection。相反的,两个方向上的 scrollView 都由各自的 ASRangeController
单独控制自己的 range 参数。
来到二次元
现在,你已经有了完整的 AnimalTableController
, 你可以把它做为 ASPagerNode 的一个page。
项目已经提前写好了控制器的代码,首先进入 AppDelegate.m
。
找到 -installRootViewController
的下面代码:
AnimalTableController *vc = [[AnimalTableController alloc]
initWithAnimals:[RainforestCardInfo allAnimals]];
替换为:
AnimalPagerController *vc = [[AnimalPagerController alloc] init];
然后,跳到 AnimalPagerController.m
在 -init
方法中添加创建 pager
方法以及 dataSource
的数据源:
_pagerNode = [[ASPagerNode alloc] init];
_pagerNode.dataSource = self;
pagerNode 是 ASCollectionNode
的子类,使用方法与 UIPageViewController
一样。API 实际上比 UIPageViewController
要简单的多。
接下来要实现 pager 的 dataSource
方法,在底部找到 ASPagerDataSource
分类.
首先,告诉 pager 有几个页面。实际上当前的 animal 数组中有三组不同动物,我们需要重写 -numberOfPagesInPagerNode:
方法:
- (NSInteger)numberOfPagesInPagerNode:(ASPagerNode *)pagerNode {
return self.animals.count;
}
然后,你需要实现 -pagerNode:nodeAtIndex
方法,类似于先前实现的 ASTableNode 的 dataSource
方法。
- (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeAtIndex:(NSInteger)index {
// 1
NSArray *animals = self.animals[index];
// 2
ASCellNode *node = [[ASCellNode alloc] initWithViewControllerBlock:^UIViewController * _Nonnull{
return [[AnimalTableController alloc] initWithAnimals:animals];
} didLoadBlock:nil];
return node;
}
我们来总结下这部分:
- 尽管这个版本中没有进行模块化分,但是首先获取数据模型是个好习惯。
- 这一次,你使用的正是强大的
-initWithViewControllerBlock:
构造器。你所要做的就是返回一个block,这个 block 返回你提前设置好的 tableNodeController,它将自动展示在pager 的 页面中。真是太酷了😏~
一旦你添加了这个方法,你将拥有一个完整功能的 Pagar,其中的 cell 是从你原先创建的 tableNodeController
生成的。现在,就可以在用户的垂直和水平滑动下,充分发挥二维预加载的功能!
要查看这个 AsyncDisplayKit 2.0 教程完整的项目,点击这里进行下载。如果你想查看swift版本,这里也有。
准备好之后,请转到该项目的第2部分,了解 AsyncDisplayKit 2.0 引入的强大的新的布局系统。
如果你想先进行深入了解,你可以阅读 AsyncDisplayKit主页 的文档。Scott Goodson(AsyncDisplayKit的原创作者)也有几个你可能会感兴趣的话题。最近的话题很好的概述了一些框架对处理大图片存在问题的的尝试。
你可能会对 Paper的构建 感兴趣。虽然当时并没有开源,并且有许多地方发生了变化,但看到这一切的开始还是挺有意思的。
这里有一个 public Skack channel ,欢迎来提问~
网友评论
UIColor *color = [(NSDictionary *)parameters objectForKey:@"color"];
这句中的第二个应该为UITableViewCell