[译] AsyncDisplayKit/Texture 官方文档(1)

7,792 阅读17分钟

官方文档链接:texturegroup.org/docs/gettin…

开始使用 Texture

Texture 的基本单位是 NodeASDisplayNodeUIViewCALayer 的抽象,它与 UIKit 的不同在于:

Node 可以异步绘制,且线程安全,你可以在在异步线程中进行实例化和配置它们的层级结构。

为了保持用户界面的流畅,你的 App 应该以 1/60 秒的帧率呈现, 这意味着主线程有 1/ 60 秒来处理一帧,也就是说,主线程需要在 16 毫秒内来执行所有的布局和绘图代码,而由于一些系统级别的开销,你的布局绘图代码一般执行超过 10 毫秒,就可能引起掉帧。

Texture 可以让你将图像解码、文本渲染和其他消耗性能的 UI 操作从主线程剥离,以保证主线程可以流畅的响应用户的交互,Texture 还有其他的使用窍门,我们稍后会讲到。

节点/Node

如果你会使用 UIView,那么你就已经知道如何使用 Node 了,UIView 中绝大部分的方法 Node 都有映射,并且 UIViewCALayer 大多数的属性,也是可以使用的。当属性和方法的命名有差异时,例如 .clipsToBounds.masksToBoundsNode 默认使用 UIView 的命名,唯一例外的是 Node 使用 .position 替代了 .center

当然,你也可以通过 node.viewnode.layer 直接调用原生属性和方法,但要确保它会在主线程上执行!

Texture 已经提供了多种多样的 Node 来替换你习惯使用的大部分 UIKit 组件,现在你已经可以完全通过 Texture 开发大规模的 App 。

节点容器/Node Containers

当你将 Texture 集成到一个项目中时,一个常见的错误是将 Node 直接添加到已有的视图中,这样做的结果是你的节点在渲染时会闪烁。

正确的做法是,你应该把节点添加到 Node 容器中,由 Node 容器负责管理这些节点,你可以把 Node 容器理解为 UIKit 和 ASDK 之间的桥梁。

布局引擎/Layout Engine

Texture 的布局非常强大,相对于传统的 Frame,AutoLayout 等方式而言比较独特,但对前端工作者并不陌生,Texture 的布局基于 CSS FlexBox。它提供了指定自定义节点的大小和其子节点布局的声明方式,当所有的节点同时被渲染时,通过为每个节点提供的 ASLayoutSpec 异步计算 size 和布局。

它和 UIStackView 很像,但支持低版本 iOS。

高级开发者功能

Texture 提供了在 UIKit 或 Foundation 中无法找到的各种高级开发功能,我们的开发人员发现,Texture 可以简化它们的架构,从而提高了开发效率。

完整列表即将推出...

没放出来说个啥?

集成 Texture

如果你第一次使用 Texture,我们建议你看看 ASDKgram 示例,我们已经撰写了一个 Guide(即将推出),指导你如何一步一步将 Texture 集成到一个 App 中。

没放出来说个啥?

如果您遇到任何问题,请到我们的 GitHub 或 Slack 寻求帮助。

资源

在 Slack 社区,和 Texture 的核心团队及 700+ Texture 开发者一起,实时 Debug、获取更新和进行交流,点此注册

示例

查看我们的示例库,如果你第一次使用 Texture,我们建议你从 ASDKgram 开始,这个示例同时使用 UIKit 和 Texture 实现了照片Feed,你可以对比查看它们的区别,它的特点是:

  1. 一个无限滚动的 Home Feed,演示 Texture 的平滑滚动性能;
  2. 一个相当庞大的代码库,用于演示使用 Texture 设计 App 可以减少多少代码;

视频

教程/文章

FlexBox 布局学习

Texture 强大的布局系统基于CSS FlexBox 模型,这些网站对于学习 FlexBox 的基本知识非常有用。

安装 Texture

Texture 可以使用 CocoaPods 和 Carthage 安装,使用时不要忘记导入头文件:

#import <AsyncDisplayKit/AsyncDisplayKit.h>

如果你使用 Swift 开发,可以使用 Objective-C bridging header 进行桥接,如果你在安装中遇到任何问题,请在 GitHub 或 Slack 联系我们。

CocoaPods

使用 CocoaPods 安装,将以下内容添加到 Podfile:

target 'MyApp' do
    pod "Texture"
end

完全退出 Xcode,打开终端,cd 到项目目录,执行下面的命令:

pod install

如果要更新 Texture,打开终端,cd 到项目目录,执行下面的命令:

pod update Texture

不要忘记使用 .xcworkspace 打开,而不是 .xcodeproj

Carthage(常规安装)

使用迦太基需要创建一个Cartfile 列表,然后执行 carthage update,将依赖项下载到 Cathage/Checkouts 文件夹中,并将它们构建到位于 Carthage/Build 文件夹中,开发人员须手工集成到项目中。

Texture 也可以通过迦太基安装,将以下内容添加到 Cartfile 以获取最新的 release 版本:

github "texturegroup/texture"

或者获取主干:

github "texturegroup/texture" "master"

Texture 有自己的 Cartfile,指明了自己的依赖项,Texture 所需的依赖会自动安装,安装 Texture 依赖需要使用终端,在Carthage/Checkouts 目录下,执行:

carthage update

确认 Texture、PINRemoteImage (3.0.0-beta.2)、PINCache 全部获取和构建,Texture 的 Cartfile 会自动处理这些依赖关系。

打开 Xcode,将所需的框架拖到 TARGETS - General -Linked Frameworks and Libraries

Carthage(light)

Texture 不支持 Carthage 更轻量的使用方式,你需要手动添加项目文件, 这是因为 Texture 的依赖 PINCache 还没有项目文件,

PINCache 是 PINRemoteImage 一个嵌套的依赖。

如果没有 PINRemoteImage 和 PINCache,你将无法完整的使用 Texture 图像功能集。

升级到 2.0

发布说明

请阅读 GitHub 上的官方发布说明。

获取正式候选版本

将以下内容添加到你的 pod 文件中:

pod 'Texture', '>= 2.0'

在终端执行:

pod repo update
pod update Texture

测试 2.0

一旦你更新到2.0,你会看到许多弃用警告。 别担心!这些警告是安全的,因为我们已经桥接了所有旧的 API,以便在迁移到新的 API 之前对 2.0 进行测试。
如果你的 App 无法构建成功而不是仅显示警告,那么你的项目中可能出现错误,你有几个选择:

  1. 在 project settings 中禁用 deprecation warnings;
  2. 在项目的 build settings 中禁用 warnings as errors;
  3. 在 Texture 中禁用 deprecation warnings,这需要你将 ASBaseDefines.h 中的第 74 行更改为 # define ASDISPLAYNODE_WARN_DEPRECATED 0

当你的 App 构建成功并运行,你需要测试它以确保一切正常工作。 如果你发现任何问题,请尝试在该区域采用新的 API 并重新测试。

你可能会注意到一个关键的变化:

ASStackLayoutSpec的.alignItems 属性默认值更改为 ASStackLayoutAlignItemsStretch 而不是 ASStackLayoutAlignItemsStart,这可能会在 UI 中造成失真。

如果还有其他问题,请提交 GitHub issue,我们很乐意帮助你!

聪明的预加载

异步并发渲染和 FlexBox 布局已经非常强大,但 Texture 做到的不止于此,另外一个重要的层面是智能预加载的思想。

正如在开始使用 Texture 这节中讲的那样,在一个节点容器的上下文之外使用一个节点有一些弊端的。这是因为所有的节点都具有当前界面状态的概念,这个状态命名为 interfaceState

interfaceState 这个属性是不断更新的,它的更新由 ASRangeController 所控制,ASRangeController 又由所有的节点容器在内部创建和维护。

在容器外部使用的节点不会被 ASRangeController 更新状态,因此这有时会导致闪烁,原因是这些容器外的节点因为状态的错误,在节点被渲染到屏幕后又进行了一次渲染。

Interface State Ranges

当将节点添加到滚动或分页界面时,它们通常位于以下范围中的一个。这意味着当滚动视图被滚动时,它们的界面状态将随着它们的移动而更新。

一个节点的所处的范围会是以下范围中的一个:

界面范围 描述
Preload 节点还不可见,这时节点收集外部源,外部源可能是 API 或者本地磁盘。
Display 节点开始渲染,包括文本的光栅化已经图像解码等。
Visible 节点可见,在屏幕上至少拥有一个像素。

ASRangeTuningParameters

每个范围的大小以整个屏幕的尺寸作为参照, 默认的 size 在许多用例中都能很好地工作,你也可以通过在滚动节点上设置范围参数来调整它们。

在上面的一个滚动集合的示例图片中,用户正在向下滚动。正如你所看到的,用户滚动方向区域(领先方向)要比用户离开方向区域(尾随方向)大得多。为了保持内存的最佳使用,当用户改变滚动方向时,两个区域会动态地交换。这使你不必考虑滚动方向的变化,只需关注领先方向和尾随方向的区域大小。

在这张图中,你可以看到智能的预加载是如何进行工作的,你可以看到在一个垂直的滚动容器中,虽然有些节点还未在设备屏幕中出现,但是它有一个范围控制器,屏幕外的节点处在 Preload 数据准备范围和 Display 渲染准备范围。

Interface State Callbacks

当用户滚动时,节点在这三个范围中切换,并通过加载数据,渲染等作出适当的反应。你创建的节点子类可以通过实现相应的回调方法进入此机制。

class TZYNode: ASDisplayNode {

    override func didEnterPreloadState() {
        print("进入数据加载范围")
    }

    override func didExitPreloadState() {
        print("离开数据加载范围")
    }

    override func didEnterDisplayState() {
        print("进入渲染范围")
    }

    override func didExitDisplayState() {
        print("离开渲染范围")
    }

    override func didEnterVisibleState() {
        print("进入可见范围")
    }

    override func didExitVisibleState() {
        print("离开可见范围")
    }

}

节点容器/Node Containers

在节点容器中使用节点

我们强烈建议你在节点容器中使用 Texture 的节点, Texture 提供以下节点容器:

节点容器 等价于 UIKit
ASCollectionNode 代替 UICollectionView
ASPagerNode 代替UIPageViewController
ASTableNode 代替UITableView
ASViewController 代替UIViewController
ASNavigationController 代替UINavigationController,实现 ASVisibility 协议。
ASTabBarController 代替UITabBarController,实现 ASVisibility 协议。

示例代码和特定示例项目会在每个节点容器的文档中突出显示。

为什么使用节点容器

节点容器自动管理其子节点实现智能预加载,这意味着节点的所有布局计算,数据读取,解码和渲染都将会异步完成,这就是为什么我们建议将节点放进节点容器中使用的原因。
请注意,尽管你可以直接使用节点而不加入节点容器,但除非你添加其他回调,否则这个容器外的节点只会在屏幕出现时才会开始渲染。如同 UIKit 所做的那样,这可能导致性能下降和内容闪烁。

节点子类/Node Subclasses

在 UIKit 组件上使用节点的一个主要优点是,所有的节点都预先在主线程布局并绘制,这样主线程就可以立即响应用户交互事件,而无需先处理控件的渲染,Texture 提供以下节点:

节点 等价于 UIKit
ASDisplayNode 代替 UIView,所有的 Node 都继承自 ASDisplayNode。
ASCellNode 代替 UITableViewCell&UICollectionViewCell,需要和 ASTableNode,ASCollectionNode 和 ASPagerNode 共同使用。
ASScrollNode 代替 UIScrollView,这个节点对于创建自定义的,包含其他节点的可滚动区域非常有用。
ASEditableTextNode 代替 UITextView。
ASTextNode 代替 UILabel。
ASImageNode 代替 UIImage。
ASNetworkImageNode 代替 UIImage。
ASMultiplexImageNode 代替 UIImage。
ASVideoNode 代替 AVPlayerLayer。
ASVideoPlayerNode 代替 UIMoviePlayer。
ASControlNode 代替 UIControl。
ASButtonNode 代替 UIButton。
ASMapNode 代替 MKMapView。

尽管与 UIKit 组件大致相当,但一般而言,Texture 节点提供了更高级的功能和便利。 例如,ASNetworkImageNode 可以自动加载网络图片和进行缓存管理,甚至支持渐进式 JPEG 和动画 GIF。

AsyncDisplayKitOverview 示例应用程序给出了上面列出的每个节点的基本实现。

节点继承关系/Node Inheritance Hierarchy

所有的 Texture 节点都继承自 ASDisplayNode

Node层级
Node层级

原图使用花体英文,查看原图

右侧的节点是 UIKit 元素的封装。 例如,ASScrollNode 封装了一个 UIScrollView,而 ASCollectionNode 封装了一个 UICollectionViewliveMapMode 中的 ASMapNodeUIMapView 的封装。

liveMapMode 未在此节和之前章节出现,不清楚是否为笔误。

节点实例/Subclassing

创建子类时最重要的区别是你使用的是 ASViewController 还是 ASDisplayNode,这听起来很明显,但是因为这其中有一些微妙的差异,所以记住这点还是相当重要的。

ASDisplayNode

虽然实例化节点与 UIView 类似,但需要遵循一些原则,以确保你充分利用了它的能力,并确保节点按照预期的方式运行。

-init

在使用 initNodeBlocks 时,这个方法会后台线程上被调用。但是,因为没有其他方法会在 init 完成之前运行,所以这个方法不需要加锁。

需要记住的最重要的一点是,init 方法必须能够在任何队列上调用。最值得注意的是,你永远不应该在节点初始化方法中初始化任何 UIKit 对象,以及调用 node.layer node.view.x 等与viewlayer 有关的操作,也不应该在这个方法中为节点添加手势,这些事件应该在 didLoad 方法中进行。

-didLoad

这个方法在概念上类似于 UIViewController-viewDidLoad 方法,当后台视图初始化完成时,它会被调用一次,它保证会在主线程上被调用,是执行任何 UIKit 代码合适的地方,例如添加手势识,更改 viewlayer,初始化 UIKit 对象。

-layoutSpecThatFits:

该方法定义了节点的布局,并在后台线程上进行了大量的计算。此方法是你声明、创建和修改 ASLayoutSpec 布局描述对象的地方,该对象描述了节点的 size,以及其子节点的 size 和 position,是你放置大部分布局代码的地方。

ASLayoutSpec 对象直到在此方法中返回前是可变的。 在这之后,这个对象将不可改变,需要注意的是你不需要缓存 ASLayoutSpec 对象以备后用,我们建议你在必要时重新创建布局描述。

由于它在后台线程上运行,因此你不能在这个方法中调用 node.viewnode.layer 以及它们的属性。 此外,除非你明确知道自己在做什么,否则不要在此方法中创建其他节点。 另外,重写此方法并不一定需要调用 super 方法。

-layout

在此方法中调用 super 将,会使用 layoutSpec 对象计算布局,所有子节点都将计算其 size 和 position。

-layout 在概念上类似于 UIViewController-viewwilllayoutsubview,这是一个更改 hidden 属性、修改 view 属性、设置背景颜色的好地方。你可以在 -layoutspec: 方法中设定背景颜色,但这可能会存在时序问题。如果你需要使用原生的 UIView,可以在这里设置它们的 frame,不管怎样,你始终可以使用 -initWithViewBlock: 创建节点,并在其他地方的后台线程中进行调整。

这个方法在主线程上被调用,如果你使用的是 ASLayoutSpec,那么你不应该过多地依赖这个方法,因为在主线程上进行布局是非常可取的,需要这个方法的子类小于 1/10。

使用 -layout 的一个重要用途是你需要子节点的 size 是精确的。举例来说,当你希望一个 collectionNode 可以铺面屏幕,这种情况不被 ASLayoutSpec 很好的支持,此时最简单的做法是在这个方法中手动设定 frame

subnode.frame = self.bounds

如果你希望在 ASViewController 中得到相同的效果,那么你可以在 -viewWillLayoutSubviews 中做同样的事情,不过如果你的节点通过 initWithNode: 进行实例化,它会自动做到这一点。

ASViewController

ASViewController 是一个常规的 UIViewController 子类,它具有管理节点的特殊功能。因为它是一个 UIViewController 子类,所以所有的方法都在主线程上被调用,并且你应该在主线程上创建至少一个 ASViewController

-init

这个方法在 ASViewController 的生命周期开始时被调用一次,与 UIViewController 的初始化一样,你最好不要在这个方法中访问 self.viewself.node.view,因为这样会强制创建 view。 这些操作可以在 -viewDidLoad 中进行,-viewDidLoad 可以执行任何 view 的访问。

ASViewController 指定的构造器是 initWithNode:,一个典型的构造器看起来就像下面的代码。请注意下面的代码,在调用 super 之前,ASViewController 的节点是如何被创建的,ASViewController 管理节点类似于 UIViewController 管理视图,但是它的初始化方式有所区别:

class TZYVC: ASViewController<ASDisplayNode> {

    init() {
        let pagerNode = ASPagerNode()
        super.init(node: pagerNode)
        pagerNode.setDataSource(self)
        pagerNode.setDelegate(self)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

extension TZYVC: ASPagerDataSource {

    func numberOfPages(in pagerNode: ASPagerNode) -> Int {
        return 10
    }

}

extension TZYVC: ASPagerDelegate {


}

-loadView

我们建议你不要使用这个方法,因为与 -viewDidLoad 相比,它没有什么特别的优势,并且有一些缺点。 但是,只要不将 self.view 属性设置为不同的值,它可以安全的使用。 它的 super 方法会将其封装的 UIViewControllerview 设置为 ASViewControllernode.view

-viewDidLoad

这个方法在 -loadView 之后被执行,这是 ASViewController 生命周期中,你可以访问 node.view 最早的方法,你可以在这份方法中任意修改 viewlayer 或添加手势,这个方法在其所属的生命周期中,只会执行一次。

所以布局代码不应该放在这个方法中,因为当界面重绘时,这里的代码不会被再次调用。UIViewController 中这个方法也是同样的,在这种方法中放置布局代码是一种不太好的做法,即使你的布局不会因为交互发生变化。

-viewWillLayoutSubviews

这个方法会与节点的 -layout 同时调用,它可能在 ASViewController 的生命周期中被多次调用,当 ASViewController 的节点的边界发生改变,如旋转、分割屏幕、键盘弹出等行为,或者当视图的层次结构发生变化,如子节点添加、删除或改变大小时,这个方法将被调用。

因为它不经常被调用,但是调用就代表页面需要重绘,因此所有的布局代码最好都放在这个方法中,即使是不直接依赖于 size 的 UI 代码也应放在这里。

-viewWillAppear: / -viewDidDisappear:

viewWillAppearASViewController 的节点出现在屏幕上之前被调用,这是节点从屏幕出现的最早时间,viewDidDisappear 在控制器从视图层次结构中移除之后被调用,这是节点从屏幕消失的最早时机,这两个方法提供了一个很好的时机来启动或停止与控制器相关的动画,这也是一个保存和记录用户行为日志的好地方。

尽管这些方法可能被多次调用,并且是可以执行布局代码的,但是这两个方法不会在所有需要重绘的时候被调用,因此除了特定的动画设置之外,不应该用于执行核心的布局代码。

未完待续

[译] AsyncDisplayKit/Texture 官方文档(2)