AsyncDisplayKit

特性

Pinterest 为了 1 亿用户的目标对其 iOS app 进行了重构。这次重构进行的主要改变是使用 AsyncDisplayKit。

ASDK 是用来保证 UIs 的流畅性和可响应性的一个开源库。它可以将图片的解码、文字大小调整及渲染,和其他比较耗时的 UI 处理放到后台线程中进行。除了异步处理这个亮点,它还提供了一种类似 CSS Box Model 的系统,让你能够像使用 UIView 一样容易的使用 CALayers (无需注册触摸事件)。另外,它还提供了滚动视图移动差值的计算,它可以在获取网络数据后,计算出用户滚动位置的 Cell 区域,提前渲染出一小部分 Cell。

Node 介绍

UIView 和 CALayer 不是线程安全的,所以无法做到在非主线程中进行操作,所以 ASDK 创建了以 node 为基本单位的对象,需要使用各种 node 来代替之前的各种控件。

node 和 UIKit 的 View 对应关系(摘要)
ASDisplayNode 代替 UIKit 的 UIView (AsyncDisplayKit 的根 node)
ASCellNode 代替 UIKit 的 UITableViewCell 和 UICollectionViewCell

ASDK 中除了 node 外,还有很多 node containers,不建议单独使用 node,而是应当将其添加到 node containers 中。

node containers 和 UIKit 的 Controller 的对应关系(摘要)
ASViewController 代替 UIKit 的 UIViewController

智能预加载

要将 node 添加到 node containers 中主要是因为所有的 node 都有它们当前接口状态的概念,这个接口状态的属性名称是 interfaceState,由所有容器创建和内部维护的 ASRangeController 进行更新。未添加到容器中的 node 不会收到该状态的更新。

例如当 nodes 被添加到可滚动或者分页的页面中,nodes 通常会在以下的范围内:
Fetch Data Range:可见的最远范围。它的内容需要从 API 或者本地磁盘中获取。
Display Range:显示任务例如文本栅格化和图片解码。
Visible Range:Node 正在屏幕上被显示的

img

这个例子是 Pinterest 主页面,中间部分有一些可以横向滚动的鼓的照片,这部分内容是不可见的,但是它们自己 Node 的 Range Controller 在 Fetch Data 和 Display 范围内。获取这些状态可以通过相关的回调方法。

性能优化

ASDK 中有一些属性,设置后可以对性能有一些提升

层的支持

不需要触摸处理的 node 只要将 layerBacked 属性设置为 yes,即可从视图转为层。通过这种方式,把一个大的层级,通过一个大的绘制方法绘制到一张图片上,性能会有很大提升。

rootNode.layerBacked = YES;

光栅化

CALayer 的 shouldRasterize 开启后,会将图层绘制到一个屏幕之外的图像,然后这个图像会被缓存起来并绘制到设计图层的 contents 和子图层。

Node 有个 类似的属性 shouldRasterizeDescendants

rootNode.shouldRasterizeDescendants = YES;

圆角设置

使用 CALayer 的 cornerRadius 属性会触发离屏渲染,当页面滚动时每帧都会执行裁剪操作,即使内容区域没有变化。

最简单有效的设置方式是使用一张中间透明圆角处和背景色相同的原角图进行覆盖,但是实际上大多时候这种方式无法应对实际需求。第二种方式就是使用 UIBezierPath 对象设置圆角区域。

ASDK 只有当开启 shouldRasterizeDescendants 时才会对 cornerRadius 属性进行优化。

绘制

UIView 的展现内容在于 CALayer 的 contents 属性,contents 属性对应着一个 Backing Image
可以将其理解为内存视图。

ASDK 就是通过 CALayer 的 delegate 控制 Backing Image 的生成,并通过 Core Graphic 方式在工作线程上,将视图及其子节点绘制到 Backing Image 上,绘制好后在 UI 线程上将这个 Backing Image 传递给 CALayer 的 contents。虽然这个过程主要依赖 Core Graphic 进行绘制,但是因为在后台线程中之行,绘制以组的方式执行减少了 Graphic Context 的切换,对于性能和流畅性没有什么影响。绘制好后如果通过 dispatch_group 的方式通知 UI 线程,当绘制节点较多时很容易造成混乱,ASDK 又引入了事务的机制,通过 Transaction 方式管理 dispatch_group 之间的关系,当 UI 线程处于空闲或即将退出时提交事务,实现方式参见 _ASAsyncTransaction,另外可以参阅 YYKit,它也借鉴了事务的方式简化了层次更利于学习。

布局

ASDK 推荐的布局方式是是用 ASLayoutSpec,ASLayoutSpec 大量借鉴了 CSS 的 FlexBox 概念。

ASDK 可以在工作线程中分批次计算 row 及其子元素布局,计算好后通过 dispatch_group_notify 通知 UI 线程将 row 插入到视图中。这里 ASDK 会根据设备 CPU 核数来分批次计算 row 的大小。

实践

由于要将 UIView 替换成 Node 势必造成大量工作,且学习曲线颇为陡峭,所以不需要大刀阔斧的去修改,只要找到合适地方,例如可能造成主线程阻塞的地方如 tableView / collectionView 等复杂视图时,将一部分性能需求较高的代码切换成 ASDK 会是一个不错的选择。

另外,运行 ASDK 的 Example 时候如果 pod 出错可以尝试更新下 CocoaPods 版本。

附上层次结构图

img

参考文章

Re-architecting Pinterest’s iOS app
AsyncDisplayKit Docs
iOS 保持界面流畅的技巧

请我喝汽水儿