AsyncDisplayKit教程

来源:互联网 发布:淘宝hd降级 编辑:程序博客网 时间:2024/05/17 18:03

原文链接:http://www.raywenderlich.com/86365/asyncdisplaykit-tutorial-achieving-60-fps-scrolling  转载请注明


Facebook 的 Paper 团队开源了一套超赞的库:AsyncDisplayKit。这个库让你的App在后台线程执行图片解码(image decoding),界面布局(layout)和渲染(rendering )等操作,这样这些操作就不会block你的用户操作(user interaction),从而让你的App拥有极其流畅的用户体验。本教程将会教你如何使用AsyncDisplayKit。

举个栗子,你可以使用AsyncDisplayKit构建任何顺滑(silky-smooth)的,每秒60帧滚动的复杂界面,这种性能挑战是UIKit优化所不能完成的。

在本教程中,你会在一个拥有严重的滚动问题的UICollectionView启动项目上,使用AsyncDisplayKit大幅提高其性能。一路上,你将学习如何在现有的项目上使用AsyncDisplayKit。

注意:

本教程要求你熟悉Swift,Core Animation和Core Graphics。

Getting Started

开始之前,先看一下AsyncDisplayKit’s intro,这会给你这个库的简要概述,以及它能解决什么问题。

当你准备好了之后,下载启动项目,编译这个项目你需要Xcode6.1和iOS 8.1SDK。

这个项目包括一个UICollectionView,UICollectionView里面是一系列包含热带雨林中发现的动物信息的卡片,每个卡片包含图片,名字和动物的描述。每个卡片的背景是主图片的模糊版本(blurred version),卡上的可视化设计细节确保文字清晰可辨。

图1


在Xcode中打开启动项目中的Layers.xcworkspace。

在本教程中,请遵循以下原则来体验使用AsyncDisplayKit带来的显著好处:

  • 在真机上运行这个app,在模拟器上运行很难看出性能的改进。
  • 这个app是通用的(universal),但是最适合运行在iPad上。
  • 最后,为了最真切的看到这个库能为你做什么,你可以用你能找到的最老的能运行IOS8的设备来运行这个应用。最好是iPad 3,因为它有一个高清屏(Retina screen),但又不是特别快。

当你选好你的设备之后,运行这个项目。你会看到如下图的界面:

图2


滑动 collection view你会注意到帧率很低,在iPad 3上差不多有15-20FPS.在本教程结束的时候,你会使帧率提升到接近60FPS!


检测响应度(Measuring Responsiveness)

在实际项目中使用AsyncDisplayKit之前你应该使用Instruments测量你的界面的响应水平,这样你就有一个基准来比较修改之后的改变。

最重要的是,你可以看到是CUP绑定还是GPU绑定(CPU-bound or GPU-bound)。这意味着是CUP还是GPU阻止了app的高帧率。这些信息会告诉你AsyncDisplayKit利用什么功能来优化您的应用程序。

如果有时间可以看下WWDC2012,从WWDC2012你可以获知滚动操作是CUP绑定的(CPU-bound),你能猜到是什么导致了帧率降低了这么多吗? 是因为图片模糊操作(blurring)阻塞了主线程。

为AsyncDisplayKit准备项目(Preparing the Project for AsyncDisplayKit)

在一个已经存在的项目中使用AsyncDisplayKit就是使用display node hierarchies替换view hierarchies和/或layer trees。Display nodes是AsyncDisplayKit的主要内容(key_tenant)。他们在视图(views)的顶层并且是线程安全的,这意味着那些通常情况下在主线程执行的操作现在可以放到后台线程中执行。这样会让你的主线程更流畅的去执行其他的操作,比如处理触摸事件或者像在本项目中处理collection view的滑动事件。也就是说,在本教程中你需要做的第一步是移除 view hierarchy.

Removing the View Hierarchy

打开RainforestCardCell.swift删除awakeFromNib()方法中所有的addSubview(...)调用,awakeFromNib()方法会变成这个样子:

override func awakeFromNib() {  super.awakeFromNib()  contentView.layer.borderColor =    UIColor(hue: 0, saturation: 0, brightness: 0.85, alpha: 0.2).CGColor  contentView.layer.borderWidth = 1}


接下来,把layoutSubviews() 方法用如下内容替换:

override func layoutSubviews() {  super.layoutSubviews()}


把configureCellDisplayWithCardInfo(cardInfo:)用如下内容替换:

func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) {  //MARK: Image Size Section  let image = UIImage(named: cardInfo.imageName)!  featureImageSizeOptional = image.size}


把RainforestCardCell中所有的视图属性(view properties)删除,会变成这个样子:

class RainforestCardCell: UICollectionViewCell {  var featureImageSizeOptional: CGSize?  ...}


最后,编译运行你会看到: 图3

现在所有的cell都是空的,他们滑动起来非常流畅。你的目标是当把所有的节点而不是视图加上之后保持这么流畅的滑动。 修改项目的每一步你都可以在真机上用Instruments测试观察你的修改是怎么影响帧率的。

添加占位图(Adding a Placeholder)


回到RainforestCardCell.swift,名为placeholderLayer的CALayer类型的变量,类型应该是显示解析可选的:

<span style="font-size:14px;">class RainforestCardCell: UICollectionViewCell {  var featureImageSizeOptional: CGSize?  var placeholderLayer: CALayer!  ...}</span>
你需要一个占位图因为显示是异步的,如果显示需要一段时间用户会看到一个空的cell,这并不友好。同样的,当你从网络端获取一张图片的时候你需要设置一张占位图让用户知道你内容还没有准备好。在这个项目中,你会在后台线程中绘制图片而不是从网络端获取。

awakeFromNib()方法中,删除contentView的设置,创建并初始化一个placeholderLayer。把它加到contentView的layer上:

<span style="font-size:14px;">override func awakeFromNib() {  super.awakeFromNib()  placeholderLayer = CALayer()  placeholderLayer.contents = UIImage(named: "cardPlaceholder")!.CGImage  placeholderLayer.contentsGravity = kCAGravityCenter  placeholderLayer.contentsScale = UIScreen.mainScreen().scale  placeholderLayer.backgroundColor = UIColor(hue: 0, saturation: 0, brightness: 0.85, alpha: 1).CGColor  contentView.layer.addSublayer(placeholderLayer)}</span>


layoutSubviews()中,你需要布局placeholderLayer,用一下代码替换layoutSubviews():

<span style="font-size:14px;">override func layoutSubviews() {  super.layoutSubviews()  placeholderLayer?.frame = bounds}</span>

变异运行,你会看到如下界面:

IMG_0003


当普通的CALayer改变他们的frame的时候会有隐式动画,这就是布局layer的时候他们会放大的原因。改变layoutSubviews的实现来修改这个问题:

<span style="font-size:14px;">override func layoutSubviews() {  super.layoutSubviews()  CATransaction.begin()  CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)  placeholderLayer?.frame = bounds  CATransaction.commit()}</span>
修改编译,你会发现已经解决了这个问题。

你的第一个Node(Your First Node)

重构这个app的第一步是为每一个UICollectionView的cell添加一个background image node,在本节你会学到如何:

  • 为一个UICollectionView的cell创建,布局和添加一个image node。
  • 使用nodes和layers处理cell重用。
  • 模糊化image node。
首先打开Layers-Bridging-Header.h导入,AsyncDisplayKit:

<span style="font-size:14px;">#import <AsyncDisplayKit/AsyncDisplayKit.h></span>

添加Background Image Node

打开RainforestCardCell.swift,用下面的代码替换configureCellDisplayWithCardInfo(cardInfo:) :

func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) {  //MARK: Image Size Section  let image = UIImage(named: cardInfo.imageName)!  featureImageSizeOptional = image.size   //MARK: Node Creation Section  let backgroundImageNode = ASImageNode()  backgroundImageNode.image = image  backgroundImageNode.contentMode = .ScaleAspectFill}


上面的代码创建并设置了一个名为backgroundImageNode的ASImageNode类型的常量。

AsyncDisplayKit附带了几个display node的类,包括ASImageNode,显示图片的时候用它。除了默认在后台解析图片,ASImageNode相当于UIImageView。

configureCellDisplayWithCardInfo(cardInfo:)底部添加代码:

backgroundImageNode.layerBacked = true
这让backgroundImageNode成为一个layer-backed node。

node可以被UIView或者CALayer的实例支持。当你需要一个处理事件的node的时候通常使用view-backed node ,当不需要处理事件,只需要展示一些内容的时候,使用 layer-backed node——它的开销比较小。

因为这篇教程的app不需要处理事件,是将要全部使用layer-backed nodes。上面的代码中,既然backgroundImageNode是一个 layer-backed node,AsyncDisplayKit将会为动物图片创建一个CALayer。

继续在configureCellDisplayWithCardInfo(cardInfo:) 的底部添加以下代码:

//MARK: Node Layout SectionbackgroundImageNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size)
这句代码使用FrameCalculator布局了backgroundImageNode。

FrameCalculator是一个封装了cell布局的辅助类,返回每个node的frame。注意每个node的布局都是手动的,没有使用Auto Layout constraints。如果你需要构建自适应的布局或者localized-driven layouts,这里一定要小心,因为node无法添加约束。

接下来,在configureCellDisplayWithCardInfo(cardInfo:)的底部添加代码:

//MARK: Node Layer and Wrap Up Sectionself.contentView.layer.addSublayer(backgroundImageNode.layer)
这句代码把backgroundImageNode的Layer加到contentView的layer上。

从技术的角度来看,layer一直都存在。但是图片的渲染是异步完成的。layer初始化的时候是空的(透明的)。一旦渲染完成,layer的内容会更新包含图片内容。

这时,cell的contentView包含两个subLayer:placeholder和node的layer,在node绘制完成之前只有placeholder显示。

注意每次一个cell出列的时候方法configureCellDisplayWithCardInfo(cardInfo:)会被调用,每次一个cell重用,这段逻辑会为cell的contentView的layer添加一个新的sublayer,别担心,我们很快会解决这个问题。

回到RainforestCardCell.swift的顶部,添加一个名叫backgroundImageNode的ASImageNode类型的变量,像这样:

class RainforestCardCell: UICollectionViewCell {  var featureImageSizeOptional: CGSize?  var placeholderLayer: CALayer!  var backgroundImageNode: ASImageNode? ///< ADD THIS LINE  ...}
你需要这个属性,因为必须有东西hold onto backgroundImageNode的引用,否则ARC会释放掉它,结果什么也显示不出来。Nodes承载他们的layers,但是layers无法承载他们的nodes。

configureCellDisplayWithCardInfo(cardInfo:)的底部添加代码:

self.backgroundImageNode = backgroundImageNode
下面是configureCellDisplayWithCardInfo(cardInfo:)方法的完整内容:

func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) {  //MARK: Image Size Section  let image = UIImage(named: cardInfo.imageName)!  featureImageSizeOptional = image.size   //MARK: Node Creation Section  let backgroundImageNode = ASImageNode()  backgroundImageNode.image = image  backgroundImageNode.contentMode = .ScaleAspectFill  backgroundImageNode.layerBacked = true   //MARK: Node Layout Section  backgroundImageNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size)   //MARK: Node Layer and Wrap Up Section  self.contentView.layer.addSublayer(backgroundImageNode.layer)  self.backgroundImageNode = backgroundImageNode}
编译运行,观察到AsyncDisplayKit异步的为layers设置图片。CPU在后台绘制layers的内容的时候你也可以滑动界面。

IMG_0006

如果你在一台老设备上运行,你会注意到图片突然弹出到它的位置上——这是爆米花特效,不是每个人都喜欢这种特效,在教程的最后我们会解决这个问题并且教你如何优雅在淡入显示图片。

处理Cell重用(Handling Cell Reuse

回到RainforestCardCell.swift中,添加一个名为contentLayer的CALayer类型的变量,这个属性应该是可选类型的(optional type):

<span style="font-size:14px;">class RainforestCardCell: UICollectionViewCell {  var featureImageSizeOptional: CGSize?  var placeholderLayer: CALayer!  var backgroundImageNode: ASImageNode?  var contentLayer: CALayer? ///< ADD THIS LINE  ...}</span>

在configureCellDisplayWithCardInfo(cardInfo:)的最末尾添加以下代码:

<span style="font-size:14px;">self.contentLayer = backgroundImageNode.layer</span>
这句代码把backgroundImageNode的layer赋值给新的contentLayer属性。


然后用下面的代码替换prepareForReuse():

<span style="font-size:14px;">override func prepareForReuse() {  super.prepareForReuse()  backgroundImageNode?.preventOrCancelDisplay = true}</span>


因为AsyncDisplayKit可以异步的绘制nodes,nodes允许你在绘制开始或者绘制的过程中取消绘制操作。无论什么时候你想阻止或者取消绘制,只需要把preventOrCancelDisplay属性设置为true即可。在本项目中你需要在cell被重用之前取消所有正在绘制的操作。

接下来在prepareForReuse()方法底部加上:

<span style="font-size:14px;">contentLayer?.removeFromSuperlayer()</span>
这句代码把contentLayer从的它的superlayer上移除。


每次cell被重用的时候,这句代码把之前cell出列的时候加上的node的layer移除,这就解决了layer堆积的问题。

在prepareForReuse()方法底部加上:

<span style="font-size:14px;">contentLayer = nilbackgroundImageNode = nil</span>
这两句代码保证了cell可以释放这些引用,ARC在需要的时候可以清理他们。

编译运行,你会看到已经没有堆积的现象,不必要的绘制也被取消了。



是时候模糊我们的图片了。


模糊图片(Blurring the Image)

为了模糊图片,你需要在iamge node的展示进程中添加额外的一步。


打开RainforestCardCell.swift,在configureCellDisplayWithCardInfo(cardInfo:)方法中的backgroundImageNode.layerBacked之后加上代码:

<span style="font-size:14px;">backgroundImageNode.imageModificationBlock = { input in  if input == nil {    return input  }  if let blurredImage = input.applyBlurWithRadius(    30,    tintColor: UIColor(white: 0.5, alpha: 0.3),    saturationDeltaFactor: 1.8,    maskImage: nil,     didCancel:{ return false }) {      return blurredImage  } else {    return image  }}</span>

ASImageNode的imageModificationBlock给你一个机会在显示之前修改图片。这是一个灵活的特性允许你做向image node添加滤镜这种操作。


在上面的代码中你使用imageModificationBlock在cell的background image上应用了一个模糊效果。这里的关键点是,image node会在后台线程绘制内容和执行闭包(closure),使主线程的运行循环运行顺畅。这个闭包传入原始图片返回修改过的图片(UIImage)。


闭包的代码使用了UIImage的一个类别(category),这个类别是Apple在WWDC2013上发布的,它在CPU上使用加速框架(Accelerate framework)模糊图片。因为图片模糊化操作消耗大量的时间和操作,所以这一版本的类别加入了取消机制(cancelation mechanism)。模糊方法会定期的调用didCancel闭包检查是否停止模糊操作。


到目前为止,上面的代码只是让didCancel返回false。接下来你会写真正的didCancel闭包。

编译运行,你会看到模糊效果:


你会注意到界面滑动起来非常流畅。

当一个cell出列的时候会启动一个模糊操作。当用户快速的滑动collectionView的时候,每个cell都被重用的很多次,同时也开启了很多模糊操作。我们的目标是当cell将要被重用的时候停掉cell上所有正在进行的模糊操作。

在prepareForReuse()方法中已经取消了node的绘制操作。但是一旦程序运行到imageModificationBlock闭包中,我们就需要设置preventOrCancelDisplay标志位,你马上就要添加这个标志位。

取消模糊操作(Canceling the Blur

为了取消正在进行中的模糊操作,你需要实现didiCancel闭包。

为imageModificationBlock添加一个捕获列表来捕获backgroundImageNode的弱引用。

<span style="font-size:14px;">backgroundImageNode.imageModificationBlock = { [weak backgroundImageNode] input in   ...}</span>

你需要一个弱引用来避免闭包和image Node之间的retain 循环。你会使用弱引用的backgroundImageNode来决定是否取消模糊操作。

是时候构建模糊操作的取消闭包了。在imageModificationBlock中加入以下代码:

backgroundImageNode.imageModificationBlock = { [weak backgroundImageNode] input in  if input == nil {    return input  }   // ADD FROM HERE...  let didCancelBlur: () -> Bool = {    var isCancelled = true    // 1    if let strongBackgroundImageNode = backgroundImageNode {      // 2      let isCancelledClosure = {        isCancelled = strongBackgroundImageNode.preventOrCancelDisplay      }       // 3      if NSThread.isMainThread() {        isCancelledClosure()      } else {        dispatch_sync(dispatch_get_main_queue(), isCancelledClosure)      }    }    return isCancelled  }  // ...TO HERE   ...}
下面来解释刚加入的代码的作用:

  1. 捕获一个backgroundImageNode的强引用。如果此时backgroundImageNode已经不存在,isCancelled会置为true,模糊操作会被取消。
  2. 因为一旦一个node创建了layer或者view,你就只能在主线程中获取node的属性,所以需要把检查是否取消模糊操作包在一个闭包里。既然你需要获取preventOrCancelDisplay标志位,这个操作就必须在主线程中进行。
  3. 最后,你需要保证isCanceldClosure必须在主线程中调用。如果你已经在主线程中直接调用即可,如果没在主线程中需要使用dispatch_sync。必须是同步调度(synchronous dispatch)因为在didCancelBlur闭包返回值之前结束isCancelledClosure设置isCancelled。

使用下面的代码替换applyBlurWithRadius闭包:

<span style="font-size:14px;">if let blurredImage = input.applyBlurWithRadius(  30,  tintColor: UIColor(white: 0.5, alpha: 0.3),  saturationDeltaFactor: 1.8,  maskImage: nil,  didCancel: didCancelBlur) {  ...}</span>


编译运行,你可能注意不到有什么不同,但是现在任何滑出屏幕的cell的模糊操作都被取消了。这意味着开销比之前减少了很多。在像iPad3这种比较慢的设备上你可能会看到一点性能改进。

现在除了背景图,你的卡片上还没有其他内容。在接下来的四节中,你会学到如何:

  • 在一个CALayer上创建一个容器node,容器node可以绘制所有的子node;
  • 构建一个node层级(node hierarchy);
  • 创建一个ASDisplayNode子类;
  • 在后台线程构建并布局node层级;

当你完成这些之后,你的app看起来会和你加AsyncDisplayKit之前一样,但是滑动起来会非常流畅。


栅格化的容器 Node

直到现在,你一直在操作 cell 内的一个单独的 Node。接下来,你将创建一个容器 Node,它会包含所有的卡片内容。

添加一个容器 Node

继续 RainforestCardCell.swift ,在 configureCellDisplayWithCardInfo(cardInfo:) 的  backgroundImageNode.imageModificationBlock 后面以及 Node Layout Section 前面添加如下代码:

//MARK: Container Node Creation Sectionlet containerNode = ASDisplayNode()containerNode.layerBacked = truecontainerNode.shouldRasterizeDescendants = truecontainerNode.borderColor = UIColor(hue: 0, saturation: 0, brightness: 0.85, alpha: 0.2).CGColorcontainerNode.borderWidth = 1


这就创建并配置了一个叫做 containerNode 的 ASDisplayNode 常量。注意这个容器的 shouldRasterizeDescendants,这是一个关于节点如何工作的提示以及一个如何让它们工作得更好地机会。

如单词 “descendants(子孙)” 所暗示的,你可以创建 AsyncDisplayKit Node 的层次结构或树,就如你可以创建 Core Animation Layer 的层次结构一样。例如,如果你有一个都是 Layer 支持的 Node 层次结构,那么 AsyncDisplayKit 将会为每个 Node 创建一个分离的 CALayer,Layer 层次结构将会和 Node 层次结构一样,如同镜像。

这听起来很熟悉:它类似于当你使用普通的 UIKit 时,Layer 层次结构镜像于 View 层次结构。然而,这个 Layer 的栈有一些不同的效果:

首先,因为是异步渲染,你就不会看到每个 Layer 一个接一个地显示。当 AsyncDisplayKit 绘制完成每个 Layer,它马上制作 Layer 的显示内容。所以如果你有一个 Layer 的绘制比其他 Layer 耗时更长,那么它将会在它们之后显示。用户会看到零碎的 Layer 组件,这个过程通常是不可见的,因为 Core Animation 会在显示任何东西之前重绘所有必须的 Layer 。

第二,有许多 Layer 能够引起性能问题。每个 CALayer 都需要一个支持存储来保存它的像素位图和内容。同样,Core Animation 必须将每个 Layer 通过 XPC 发给渲染服务器。最后,渲染服务器可能需要重绘一些 Layer 以复合它们,例如在混合 Layer 时。总的来说,更多的 Layer 意味着 Core Animation 更多的工作。所以限制 Layer 使用的数量有许多不同的好处。

为了解决这个问题,AsyncDisplayKit 有一个方便的特性:它允许你绘制一个 Node 层次结构到一个单独的 Layer 容器里。这就是 shouldRasterizeDescendants 所做的。当你设置它,那在完成所有的 Subnode 的绘制之前,ASDisplayNode 将不会设置 Layer 的 contents。

所以在之前的步骤里,设置容器 Node 的 shouldRasterizeDescendants 为 true 有两个好处:

1. 它确保卡片一次显示所有的 Node,如同旧的同步绘制;

2. 而且它通过栅格化 Layer 栈为单个 Layer 并较少未来的合成而提高了效率。

不足之处是,由于你将所有的 Layer 放入一个位图,你就不能在之后单独动画某个 Node 了。

要获得更多信息,请看 shouldRasterizeDescendants 在头文件 ASDisplayNode.h 里的注释。

接下来,在 Container Node Creation Section 后,添加 backgroundImageNode 为 containerNode 的 Subnode:

//MARK: Node Hierarchy SectioncontainerNode.addSubnode(backgroundImageNode)

注意:添加 Node 的顺序很重要,就如同 subview 和 sublayer。最先添加的 Node 会被之后添加的阻挡显示。

替换 Node Layout Section 的第一行为:

//MARK: Node Layout SectioncontainerNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size)

最后,使用 FrameCalculator 布局 backgroundImageNode:

backgroundImageNode.frame = FrameCalculator.frameForBackgroundImage(  containerBounds: containerNode.bounds)

这设置 backgroundImageNode 填满整个 containerNode。

你几乎完成了新的 Node 层次结构,但首先你需要正确地设置 Layer 层次结构,因为容器 Node 现在是根。


管理容器 Node 的 Layer

在  Node Layer and Wrap Up Section ,将 backgroundImageNode 的 Layer 添加到 containerNode 的 Layer 上而不是 contentView 的 Layer 上:

// Replace the following line...// self.contentView.layer.addSublayer(backgroundImageNode.layer)// ...with this line:self.contentView.layer.addSublayer(containerNode.layer)

删除下面的  backgroundImageNode 保留:

self.backgroundImageNode = backgroundImageNode

因为 cell 只需要单独保留容器 Node ,所以你要移除 backgroundImageNode 属性。

不再设置 cell 的 contentLayer 属性为  backgroundImageNode 的 Layer,现在将其设置为 containerNode 的 Layer:

// Replace the following line...// self.contentLayer = backgroundImageNode.layer// ...with this line:self.contentLayer = containerNode.layer

给 RainforestCardCell 添加一个可选的 ASDisplayNode 实例存储为属性 containerNode:

class RainforestCardCell: UICollectionViewCell {  var featureImageSizeOptional: CGSize?  var placeholderLayer: CALayer!  var backgroundImageNode: ASImageNode?  var contentLayer: CALayer?  var containerNode: ASDisplayNode? ///< ADD THIS LINE  ...}
记住你需要保留你自己的 Node ,如果你不这么做它们就会被立即释放。

回到 configureCellDisplayWithCardInfo(cardInfo:),在 Node Layer and Wrap Up Section 最后,设置 containerNode 属性为 containerNode  常量:

self.containerNode = containerNode

编译并运行。模糊的图像将会再此显示!但还有最后一件事要去改变,因为现在有了新的 Node 层次结构。回忆之前 cell 重用时你将图像停止显示。现在你需要让整个 Node 层次结构停止显示。


在新的 Node 层次结构上处理 Cell 重用

继续 RainforestCardCell.swift ,在 prepareForReuse() 里,替换设置 backgroundImageNode.preventOrCancelDisplay 为在 containerNode 上调用 recursiveSetPreventOrCancelDisplay(...) 并传递 true:

override func prepareForReuse() {  super.prepareForReuse()   // Replace this line...  // backgroundImageNode?.preventOrCancelDisplay = true  // ...with this line:  containerNode?.recursiveSetPreventOrCancelDisplay(true)   contentLayer?.removeFromSuperlayer()  ...}

当你要取消整个 Node 层次结构的绘制,就使用 recursiveSetPreventOrCancelDisplay()。这个方法将会设置这个 Node 以及其所有子 Node 的 preventOrCancelDisplay 属性,无论 true 或 false。

接下来,依然在 prepareForReuse(),用设置  containerNode 为 nil 替换设置 backgroundImageNode 为 nil:

override func prepareForReuse() {  ...  contentLayer = nil   // Replace this line...  // backgroundImageNode = nil  // ...with this line:  containerNode = nil}
移除 RainforestCardCell 的 backgroundImageNode 属性:

class RainforestCardCell: UICollectionViewCell {  var featureImageSizeOptional: CGSize?  var placeholderLayer: CALayer!  // var backgroundImageNode: ASImageNode? ///< REMOVE THIS LINE  var contentLayer: CALayer?  var containerNode: ASDisplayNode?  ...}

编译并运行。这个 App 就如之前一样,但现在你的图像 Node 在容器 Node 内,而重用依然和它应有的方式一样。

09.png


Cell 内容

目前为止你有了一个 Node 层次结构,但容器内还只有一个 Node——图像 Node。现在是时候设置 Node 层次结构去复制在添加 AsyncDisplayKit 之前时应用的视图层次结构了。这意味着添加 text 和一个未模糊的特征图像。

添加特征图像

我们要添加特征图像了,它是一个未模糊的图像,显示在卡片的顶部。

打开 RainforestCardCell.swift  并找到 configureCellDisplayWithCardInfo(cardInfo:)。在 Node Creation Section 的底部,添加如下代码:

let featureImageNode = ASImageNode()featureImageNode.layerBacked = truefeatureImageNode.contentMode = .ScaleAspectFitfeatureImageNode.image = image

这会创建并配置一个叫做 featureImageNode 的 ASImageNode 常量。它被设置为 Layer 支持的,放大以适用,并设置显示图像,这次不需要模糊。

在 Node Hierarchy Section 的最后,添加 featureImageNode 为 containerNode 的 Subnode:

containerNode.addSubnode(featureImageNode)

你正在用更多 Node 填充容器哦!

在 Node Layout Section ,使用 FrameCalculator 布局  featureImageNode:

featureImageNode.frame = FrameCalculator.frameForFeatureImage(  featureImageSize: image.size,  containerFrameWidth: containerNode.frame.size.width)

编译并运行。你就会看到特征图像在卡片的顶部出现,位于模糊图像的上方。注意特征图像和模糊图像是如何在同一时间跳出。这是你之前添加的 shouldRasterizeDescendants 在起作用。

10.png


添加 Title 文本

接下来添加文字 Label,以显示动物的名字和描述。首先来动物名字吧。

继续 configureCellDisplayWithCardInfo(cardInfo:),找到 Node Creation Section 。添加下列代码到这节尾部,就在创建 featureImageNode 之后:

let titleTextNode = ASTextNode()titleTextNode.layerBacked = truetitleTextNode.backgroundColor = UIColor.clearColor()titleTextNode.attributedString = NSAttributedString.attributedStringForTitleText(cardInfo.name)

这就创建了一个叫做 titleTextNode 的 ASTextNode 常量。

ASTextNode 是另一个 AsyncDisplayKit 提供的 Node 子类,其用于显示文本。它是一个具有 UILabel 效果的 Node。它接受一个 attributedString,由 TextKit 支持,有许多特性如文本链接。要学到更多关于这个 Node 的功能,去看 ASTextNode.h 吧。

初始项目包含有一个 NSAttributedString 的扩展,它提供了一个工厂方法去生成一个属性字符串用于 Title 和 Description 文本以显示在雨林卡片上。上面的代码使用了这个扩展的 attributedStringForTitleText(...) 方法。

现在,在 Node Hierarchy Section 底部,添加如下代码:

containerNode.addSubnode(titleTextNode)

这就添加了 titleTextNode 到 Node 层次结构里。它将位于特征图像和背景图像之上,因为它在它们之后添加。

在 Node Layout Section 底部添加如下代码:

titleTextNode.frame = FrameCalculator.frameForTitleText(  containerBounds: containerNode.bounds,  featureImageFrame: featureImageNode.frame)

一样使用 FrameCalculator 布局 titleTextNode,就像 backgroundImageNode 和 featureImageNode 那样。

编译并运行。你就有了一个 Title 显示在特征图像的顶部。再次说明, Label 只会在整个 cell 准备好渲染时才渲染。

11.png

添加 Description 文本

添加一个有着 Description 文本的 Node 和添加 Title 文本的 Node 类似。

回到 configureCellDisplayWithCardInfo(cardInfo:) ,在 Node Creation Section 最后,添加如下代码。就在之前创建 titleTextNode 的语句之后:

let descriptionTextNode = ASTextNode()descriptionTextNode.layerBacked = truedescriptionTextNode.backgroundColor = UIColor.clearColor()descriptionTextNode.attributedString =   NSAttributedString.attributedStringForDescriptionText(cardInfo.description)

这就创建并配置了一个叫做 descriptionTextNode 的 ASTextNode 实例。

在  Node Hierarchy Section 最后,添加 descriptionTextNode 到 containerNode:

containerNode.addSubnode(descriptionTextNode)

在 Node Layout Section ,一样使用 FrameCalculator 布局 descriptionTextNode:

descriptionTextNode.frame = FrameCalculator.frameForDescriptionText(  containerBounds: containerNode.bounds,  featureImageFrame: featureImageNode.frame)
编译并运行。现在你能看到 Description 文本了。
12.png



自定义 Node 子类


恭喜!在你需要高性能地滑动你的用户界面的时候,你有了另外一个工具在手。

在本教程里,你通过替换视图层次结构为一个栅格化的 AsyncDisplayKit Node 层次结构,显著改善了一个性能很差的 Collection View 的滑动性能。多么令人激动!

这只是一个例子而已。AsyncDisplayKit 保有提高 UI 性能到一定水平的承诺,这通过平常的 UIKit 优化往往难以达到。

实际说来,要充分利用 AsyncDisplayKit,你需要对标准 UIKit 的真正性能瓶颈的所在有足够的了解。AsyncDisplayKit 很棒的一点是它引发我们探讨这些问题并思考我们的 App 能如何在物理的极限上更快以及更具响应性。

AsyncDisplayKit 是探讨此性能前沿的一个非常强大的工具。明智地使用它,并步步逼近超级响应UI的极限。

这仅仅是 AsyncDisplayKit 的一个开始!它作者和贡献者每天都在构建新的特性。请关注 1.1 版的 ASCollectionView 以及 ASMultiplexImageNode。从头文件中可看到“ASMultiplexImageNode 是一个图像 Node,它能加载并显示一个图像的多个版本。例如,它可以在高分辨率的图像还在渲染时先显示一个低分辨率的图像。” 非常酷,对吧 :]

你可以在此下载最终的 Xcode 项目。

AsyncDisplayKit 的指导在这里,AsyncDisplayKit 的 Github 仓库在这里。

这个库的作者在收集 API 设计的反馈。你可以在 Facebook 上 的 Paper Engineering Community group 分享你的想法,或者直接参与到 AsyncDisplayKit 的开发中,通过 GitHub 贡献你的 pull request。

目前为止,你使用了 ASImageNode 和 ASTextNode。这会带你走很远,但有些时候你需要你自己的 Node,就如同某些时候在传统的 UIKit 编程里你需要自己的 View 一样。

创建梯度 Node 类

接下来,你将给 GradientView.swift 添加 Core Graphics 代码来构建一个自定义的梯度 Display Node。这会被用于创建一个绘制梯度的自定义 Node 。梯度图会显示在特征图像的底部以便让 Title 看起来更加明显。

打开 Layers-Bridging-Header.h 并添加如下代码:

#import

需这一步是因为这个类没有包含在库的主头文件里。你在子类化任何 ASDisplayNode 或 _ASDisplayLayer 时都需要访问这个类。

菜单 File\New\File… 。选择 iOS\Source\Cocoa Touch Class 。命名类为 GradientNode 并使其作为 ASDisplayNode 的子类。选择 Swift 语言并点击 Next 。保存文件再打开 GradientNode.swift 。

添加如下方法到这个类:

class func drawRect(bounds: CGRect, withParameters parameters: NSObjectProtocol!,    isCancelled isCancelledBlock: asdisplaynode_iscancelled_block_t!, isRasterizing: Bool) { }

如同 UIView 或 CALayer,你可以子类化 ASDisplayNode 去做自定义绘制。你可以使用如同用于 UIView 的 Layer 或单独的 CALayer 的绘制代码,这取决于客户 Node 如何配置 Node。查看 ASDisplayNode+Subclasses.h 获取更多关于子类化 ASDisplayNode 的信息。

进一步,ASDisplayNode 的绘制方法比在 UIView 和 CALayer 里的接受更多参数,给你提供方法少做工作,并更有效率。

要为你的自定义 Display Node 填充内容,你需要实现来自 _ASDisplayLayerDelegate 协议的 drawRect(...) 或 displayWithParameters(...)。在继续之前,看看 _ASDisplayLayer.h 得到这个方法和它们参数的信息。搜索 _ASDisplayLayerDelegate。重点看看头文件注释里关于 drawRect(...) 的描述。

因为梯度图位于特征图的上方,使用 Core Graphics 绘制,所以你需要使用 drawRect(...) 。

打开 GradientView.swift 并拷贝 drawRect(...) 的内容到 GradientNode.swift 的 drawRect(...),如下:

class func drawRect(bounds: CGRect, withParameters parameters: NSObjectProtocol!,    isCancelled isCancelledBlock: asdisplaynode_iscancelled_block_t!, isRasterizing: Bool) {  let myContext = UIGraphicsGetCurrentContext()  CGContextSaveGState(myContext)  CGContextClipToRect(myContext, bounds)   let componentCount: UInt = 2  let locations: [CGFloat] = [0.0, 1.0]  let components: [CGFloat] = [0.0, 0.0, 0.0, 1.0,    0.0, 0.0, 0.0, 0.0]  let myColorSpace = CGColorSpaceCreateDeviceRGB()  let myGradient = CGGradientCreateWithColorComponents(myColorSpace, components,    locations, componentCount)   let myStartPoint = CGPoint(x: bounds.midX, y: bounds.maxY)  let myEndPoint = CGPoint(x: bounds.midX, y: bounds.midY)  CGContextDrawLinearGradient(myContext, myGradient, myStartPoint,    myEndPoint, UInt32(kCGGradientDrawsAfterEndLocation))   CGContextRestoreGState(myContext)}


然后删除 GradientView.swift,编译并确保没有错误。


添加梯度 Node

打开 RainforestCardCell.swift 并找到 configureCellDisplayWithCardInfo(cardInfo:)。在 Node Creation Section 底部,添加如下代码,就在创建 descriptionTextNode 的代码之后:

let gradientNode = GradientNode()gradientNode.opaque = falsegradientNode.layerBacked = true


这就创建了一个叫做 gradientNode 的 GradientNode 常量。

在 Node Hierarchy Section,在添加 featureImageNode 那样下面,添加 gradientNode 到 containerNode:

//MARK: Node Hierarchy SectioncontainerNode.addSubnode(backgroundImageNode)containerNode.addSubnode(featureImageNode)containerNode.addSubnode(gradientNode) ///< ADD THIS LINEcontainerNode.addSubnode(titleTextNode)containerNode.addSubnode(descriptionTextNode)


梯度 Node 需要这个位置才能在特征图之上,Title 之下。

然后添加如下代码到 Node Layout Section 底部:

gradientNode.frame = FrameCalculator.frameForGradient(  featureImageFrame: featureImageNode.frame)

编译并运行。你将看到梯度在特征图的底部。Title 确实看得更清楚了!

13.png



爆米花特效

如之前提到的,cell 的 Node 内容会在完成绘制时“弹出”。这不是很理想。所以让我们继续,以修复这个问题。但首先,更加深入 AsyncDisplayKit 以看看它是如何工作的。

在 configureCellDisplayWithCardInfo(cardInfo:) 的 Container Node Creation Section ,关闭容器 Node 的 shouldRasterizeDescendants:

containerNode.shouldRasterizeDescendants = false


编译并运行。你会注意到现在容器层次结构里不同的 Node 一个接一个的弹出。你会看到文字弹出,然后是特征图,然后是模糊背景图。

当 shouldRasterizeDescendants 关闭后,AsyncDisplayKit 就不是绘制一个容器 Layer 了,它会创建一个镜像卡片 Node 层次结构的 Layer 树。记得爆米花特效存在是因为每个 Layer 都在它绘制结束后立即出现,而某些 Layer 比另外一个花费更多时间在绘制上。

这不是我们所需要的,但它描述了 AsyncDisplayKit 的工作方式。我们不想要这个行为,所以还是将 shouldRasterizeDescendants 打开:

containerNode.shouldRasterizeDescendants = true

编译并运行。又回到整个 cell 在其渲染结束后弹出了。

该重新思考如何摆脱爆米花特效了。但首先,让我们看看 Node 在后台如何构造。


在后台构造 Node

除了异步地绘制,使用 AsyncDisplayKit,你同样可以异步地创建、配置以及布局。深呼吸一下,因为这就是你接下来要做的事情。

创建一个 Node 构造操作(Operation)

你要将 Node 层次结构的构造包装到一个 NSOperation 中。这样做很棒,因为这个操作能很容易的在不同的操作队列上执行,包括后台队列。

打开  RainforestCardCell.swift 。然后添加如下方法:

func nodeConstructionOperationWithCardInfo(cardInfo: RainforestCardInfo, image: UIImage) -> NSOperation {  let nodeConstructionOperation = NSBlockOperation()  nodeConstructionOperation.addExecutionBlock {     // TODO: Add node hierarchy construction  }  return nodeConstructionOperation}

绘制并不是唯一会拖慢主线程的操作。对于复杂的屏幕,布局计算也有可能变的昂贵。目前为止,本教程当前状态的项目,一个缓慢的 Node 布局会引起  Collection View 丢帧。

60 FPS 意味着你有大约 17ms 的时间让你的 cell 准备好显示,否则一个或多个帧就会被丢掉。这在 Table View 和 Collection View 有很复杂的 cell 时是非常常见的,滑动时丢帧就是这个原因。


AsyncDisplayKit 前来救援!

你将使用上面的 nodeConstructionOperation 将所有 Node 层次结构构造以及布局从主线程剥离并放入后台 NSOperationQueue,进一步确保 Collection View 能尽量以接近 60 FPS 的帧率滑动。

警告:你可以在后台访问并设置 Node 的属性,但只能在 Node 的 Layer 或 View 被创建之前,也就是当你第一次访问 Node 的 Layer 或 View 属性时。

一旦 Node 的 Layer 或 View 被创建,你必须在主线程才能访问和设置 Node 的属性,因为 Node 将会转发这些调用到它的 Layer 或 View。如果你得到一个崩溃 log 说“Incorrect display node thread affinity”,那就意味着在创建 Node 的 Layer 或 View 之后,你依然尝试在后台访问或设置 Node 的属性。

修改 nodeConstructionOperation 操作 Block 的内容如下:

nodeConstructionOperation.addExecutionBlock {  [weak self, unowned nodeConstructionOperation] in  if nodeConstructionOperation.cancelled {    return  }  if let strongSelf = self {    // TODO: Add node hierarchy construction  }}

在这个操作运行时,cell 可能已经被释放了。在那种情况下,你不需要做任何工作。类似的,如果操作被取消了,那一样也没有工作要做了。

之所以对 nodeConstructionOperation` 使用 unowned  引用是为了避免在操作和执行闭包之间产生保留环。

现在找到 configureCellDisplayWithCardInfo(cardInfo:)。将任何在 Image Size Section 之后的代码移动到 nodeConstructionOperation 的执行闭包里。将代码放在 strongSelf 的条件语句里,即TODO的位置。之后 configureCellDisplayWithCardInfo(cardInfo:) 将看起来如下:

func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) {  //MARK: Image Size Section  let image = UIImage(named: cardInfo.imageName)!  featureImageSizeOptional = image.size}

目前,你会有一些编译错误。这是因为操作 Block 里的 self 是 weak 引用,因此是可选的。但你有一个 self 的 strong 引用,因为代码在可选绑定语句内。所以替换错误的几行成下面的样子:

strongSelf.contentView.layer.addSublayer(containerNode.layer)strongSelf.contentLayer = containerNode.layerstrongSelf.containerNode = containerNode
最后,添加如下代码到你刚改动的三行之下:

containerNode.setNeedsDisplay()

编译确保没有错误。如果你现在运行,那么只有占位图会显示,因为 Node 的创建操作还没有实际使用。让我们来添加它。


使用 Node 创建操作

打开 RainforestCardCell.swift 并添加如下属性:

class RainforestCardCell: UICollectionViewCell {  var featureImageSizeOptional: CGSize?  var placeholderLayer: CALayer!  var backgroundImageNode: ASImageNode?  var contentLayer: CALayer?  var containerNode: ASDisplayNode?  var nodeConstructionOperation: NSOperation? ///< ADD THIS LINE  ...}

这就添加了一个叫做 nodeConstructionOperation 的可选属性

当 cell 准备回收时,你会使用这个属性去取消 Node 的构造。这会在用户非常快速地滑动 Collection View 时发生,特别是如果布局还需要一些计算时间的话。

在 prepareForReuse() 添加如下指示的代码:

override func prepareForReuse() {  super.prepareForReuse()   // ADD FROM HERE...  if let operation = nodeConstructionOperation {    operation.cancel()  }  // ...TO HERE   containerNode?.recursiveSetPreventOrCancelDisplay(true)  contentLayer?.removeFromSuperlayer()  contentLayer = nil  containerNode = nil}

这就在 cell 重用时取消了操作,所以如果 Node 创建还没完成,它也不会完成。

现在找到 configureCellDisplayWithCardInfo(cardInfo:) 并添加如下指示的代码:

func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) {  // ADD FROM HERE...  if let oldNodeConstructionOperation = nodeConstructionOperation {    oldNodeConstructionOperation.cancel()  }  // ...TO HERE   //MARK: Image Size Section  let image = UIImage(named: cardInfo.imageName)!  featureImageSizeOptional = image.size}

这个 cell 现在会在它准备重用并开始配置时,取消任何进行中的 Node 构造操作。这确保了操作被取消,即使 cell 在准备好重用前就被重新配置。

编译并确保没有错误。

在主线程运行

AsyncDisplayKit 允许你在非主线程做许多工作。但当它要面对 UIKit 和 CoreAnimation 时,你还是需要在主线程做。目前为止,你从主线程移走了所有的 Node 创建。但还有一件事需要被放在主线程——即设置 CoreAnimation 的 Layer 层次结构。

在 RainforestCardCell.swift 里,找到 nodeConstructionOperationWithCardInfo(cardInfo:image:) 并替换 Node Layer and Wrap Up Section 为如下代码:

// 1dispatch_async(dispatch_get_main_queue()) { [weak nodeConstructionOperation] in  if let strongNodeConstructionOperation = nodeConstructionOperation {    // 2    if strongNodeConstructionOperation.cancelled {      return    }     // 3    if strongSelf.nodeConstructionOperation !== strongNodeConstructionOperation {      return    }     // 4    if containerNode.preventOrCancelDisplay {      return    }     // 5    //MARK: Node Layer and Wrap Up Section    strongSelf.contentView.layer.addSublayer(containerNode.layer)    containerNode.setNeedsDisplay()    strongSelf.contentLayer = containerNode.layer    strongSelf.containerNode = containerNode  }}

下面描述一下:

1. 回忆到当 Node 的 Layer 属性被第一个访问时,所有的 Layer 会被创建。这就是为何你必须运行 Node Layer 并在主线程包装小节,因此代码访问 Node 的 Layer。

2. 操作被检查以确定是否在添加 Layer 之前就已经取消了。在操作完成前,cell 被重用或者重新配置,就很可能会出现这样的情况,那你就不应该添加 Layer 了。

3. 作为一个保险,确保 Node 当前的 nodeConstructionOperation 和调度此闭包的操作是同一个 NSOperation 。

4. 如果 containerNode 的 preventOrCancel 是 true 就立即返回。如果构造操作完成,但 Node 的绘制还没有被取消,你依然不想 Node 的 Layer 显示在 cell 里。

5. 最后,添加 Node 的 Layer 到层次结构中,如果必要,这将创建 Layer。

编译确保没有错误。

开始 Node 创建操作

你依然没有 实际 创建和开始操作。让我们现在来来吧。

继续在 RainforestCardCell.swift 里,改变 configureCellDisplayWithCardInfo(cardInfo:) 的方法签名为:

func configureCellDisplayWithCardInfo(  cardInfo: RainforestCardInfo,  nodeConstructionQueue: NSOperationQueue)

这里添加了一个新的参数 nodeConstructionQueue。它就是一个用于 Node 创建操作的入队的 NSOperationQueue 。

在 configureCellDisplayWithCardInfo(cardInfo:nodeConstructionQueue:) 底部,添加如下代码:

let newNodeConstructionOperation = nodeConstructionOperationWithCardInfo(cardInfo, image: image)nodeConstructionOperation = newNodeConstructionOperationnodeConstructionQueue.addOperation(newNodeConstructionOperation)

这就创建了一个 Node 构造操作,将其保留在 nodeConstructionOperation 属性,并将其添加到传入的队列。

最后,打开 RainforestViewController.swift 。给 RainforestViewController 添加一个叫做 nodeConstructionQueue 的初始化为常量的属性,如下:

class RainforestViewController: UICollectionViewController {  let rainforestCardsInfo = getAllCardInfo()  let nodeConstructionQueue = NSOperationQueue() ///< ADD THIS LINE  ...}

接下来,在 collectionView(collectionView:cellForItemAtIndexPath indexPath:) 里,传递 View Controller 的 nodeConstructionQueue 到 configureCellDisplayWithCardInfo(cardInfo:nodeConstructionQueue:) :

cell.configureCellDisplayWithCardInfo(cardInfo, nodeConstructionQueue: nodeConstructionQueue)

cell 将会创建一个新的 Node 构造操作并将其添加到 View Controller 的操作队列里并发运行。记住在 cell 出队时就会创建一个新 Node 层次结构。这并不理想,但足够好。如果你要缓存 Node 的重用,看看 ASRangeController 吧。

哦呼,OK,现在编译并运行!你将看到和之前一样的效果,但现在布局和渲染都没在主线程执行了。牛!我打赌里你重来没有想过你会看到这一天你所做的 事情。这就是 AsyncDisplayKit 的威力。你可以将更多更多不需要在主线程的操作从主线程移除,这将给主线程更多机会处理用户交互,让你的 App 摸起来如黄油般顺滑。

14.png

淡入 Cell

现在是有趣的部分。在这个简短的小节,你将学到:

· 用自定义 Display Layer 子类来支持 Node;

· 触发 Node Layer 的隐式动画。

这将会确保你移除爆米花特效并最终带来良好的淡入动画。

创建一个新的 Layer 子类。

菜单 File\New\File… ,选择 iOS\Source\Cocoa Touch Class 并单击 Next 。命名类为 AnimatedContentsDisplayLayer 并使其作为 _ASDisplayLayer 的子类。选择 Swift 语言并单击 Next。最后保存并打开 AnimatedContentsDisplayLayer.swift 。

现在添加如下方法到类:

override func actionForKey(event: String!) -> CAAction! {  if let action = super.actionForKey(event) {    return action  }   if event == "contents" && contents == nil {    let transition = CATransition()    transition.duration = 0.6    transition.type = kCATransitionFade    return transition  }   return nil}

Layer 有一个 contents 属性,它告诉系统为这个 Layer 绘制什么。AsyncDisplayKit 通过在后台渲染 contents 并最后在主线程设置 contents。

这个代码将会添加一个过渡动画,这样 contents 就会淡如到 View 中。你可以在 Apple 的 Core Animation Programming Guide 找到更多关于隐式 Layer 动画以及 CAAction 的信息.。

编译并确保没有错误。


淡入容器 Node

你已经设置好一个 Layer 会在其 contents 被设置时淡入,你现在就要使用这个 Layer。

打开 RainforestCardCell.swift 。在 nodeConstructionOperationWithCardInfo(cardInfo:image:) 里,在 Container Node Creation Section 开头,改动如下行:

// REPLACE THIS LINE...// let containerNode = ASDisplayNode()// ...WITH THIS LINE:let containerNode = ASDisplayNode(layerClass: AnimatedContentsDisplayLayer.self)

这会告诉容器 Node 使用 AnimatedContentsDisplayLayer 实例作为其支持 Layer,因此自动带来淡入的效果。

注意:只有 _ASDisplayLayer 的子类才能被异步地绘制。

编译并运行。你将看到容器 Node 会在其绘制好之后淡入。

15.png


何去何从

恭喜!在你需要高性能地滑动你的用户界面的时候,你有了另外一个工具在手。

在本教程里,你通过替换视图层次结构为一个栅格化的 AsyncDisplayKit Node 层次结构,显著改善了一个性能很差的 Collection View 的滑动性能。多么令人激动!

这只是一个例子而已。AsyncDisplayKit 保有提高 UI 性能到一定水平的承诺,这通过平常的 UIKit 优化往往难以达到。

实际说来,要充分利用 AsyncDisplayKit,你需要对标准 UIKit 的真正性能瓶颈的所在有足够的了解。AsyncDisplayKit 很棒的一点是它引发我们探讨这些问题并思考我们的 App 能如何在物理的极限上更快以及更具响应性。

AsyncDisplayKit 是探讨此性能前沿的一个非常强大的工具。明智地使用它,并步步逼近超级响应UI的极限。

这仅仅是 AsyncDisplayKit 的一个开始!它作者和贡献者每天都在构建新的特性。请关注 1.1 版的 ASCollectionView 以及 ASMultiplexImageNode。从头文件中可看到“ASMultiplexImageNode 是一个图像 Node,它能加载并显示一个图像的多个版本。例如,它可以在高分辨率的图像还在渲染时先显示一个低分辨率的图像。” 非常酷,对吧 :]

你可以在此下载最终的 Xcode 项目。

AsyncDisplayKit 的指导在这里,AsyncDisplayKit 的 Github 仓库在这里。

这个库的作者在收集 API 设计的反馈。你可以在 Facebook 上 的 Paper Engineering Community group 分享你的想法,或者直接参与到 AsyncDisplayKit 的开发中,通过 GitHub 贡献你的 pull request。



1 0