Skip to content

Texture 核心概念

pro648 edited this page Dec 16, 2019 · 1 revision

这是 Texture 文档系列翻译,其中结合了自己的理解和工作中的使用体会。如果哪里有误,希望指出。

  1. Texture 核心概念

  2. Texture 布局 Layout

  3. Texture 便捷方法

  4. Texture 性能优化

  5. Texture 容器 Node Containers

  6. Texture 基本控件 Node

  7. Texture 中 Node 的生命周期

Texture是一个UI框架,最初诞生于Facebook的Paper app。通过使 UI 相关操作线程安全来优化app,这意味着能够将所有昂贵视图相关操作转移到后台线程,在显示前进行前期准备。

Texture之前名称为AsyncDisplayKit

Texture 的基本单位是nodeASDisplayNode是对UIView的抽象,进而也是对CALayer的抽象。node是线程安全的,因此可以在后台并行处理。

为了保持用户界面流畅和响应速度,app需要以60帧 (frame) 每秒速度绘制,即主线程需要在十六分之一秒绘制一帧,布局和绘制需要在16毫秒内完成,而且由于系统开销,代码通常只有不到十毫秒运行时间,超过这个时间就会导致掉帧。

Texture 使您可以将图片解码、文本大小计算和渲染,以及其他昂贵的 UI 操作从主线程中移除,以保持主线程可响应用户交互。

安装 Texture

可以通过 CocoaPods 或 Carthage 将 Texture 添加到项目中,如果使用 CocoaPods 添加,则将以下内容添加到 Podfile:

target 'TestVideo' do
  pod 'Texture'
end

如果你对 Cocoapods 还不了解,可以查看CocoaPods的安装与使用使用CocoaPods创建公开、私有pod这两篇文章。

核心概念

智能预加载

Node 的异步、并发进行渲染和计算功能非常强大,但 Texture 的另一个至关重要的功能是智能预加载。

需要注意的是,node 需要在 node container 中使用才拥有上述优势。在 node container 之外使用几乎没有好处,这是因为所有 node 都具有其当前界面状态的概念。

interfaceState属性由ASRangeController不断更新,所有容器都在内部创建和维护ASRangeController对象。

在容器之外使用 node 时,将没有ASRangeController负责更新其状态。这种情况下,node 可能未收到任何提醒就已经显示在了屏幕上,此时渲染 node 会出现闪烁。

界面状态改变 Interface State Ranges

将 node 添加到滚动或翻页视图时,其通常处于以下状态之一。这意味着,随着滚动视图的滚动,它们的界面状态将随着它们的移动而更新。

InterfaceStateRanges

Node 将处于以下状态之一:

界面状态 介绍
Preload 离可见范围最远。此时从外部资源加载内容,例如:从 API 或磁盘加载图片。
Display 绘制。例如文本光栅化、图片解码等。
Visible 显示,至少需要有一像素。

如上图所示,collection view 正在向下滚动,前导方向范围大小比尾随方向大很多。如果用户更改滑动方向,则前导和尾随将动态交换,以使内存使用保持最佳状态。这样可以避免定义前导和尾随大小,也不必对用户不断变化的滚动方向做出反应。

此外,range controller 还会根据界面状态、滚动视图滑动方向、渲染引擎等自动调整当前的ASLayoutRangeMode

/**
 * Each mode has a complete set of tuning parameters for range types.
 * Depending on some conditions (including interface state and direction of the scroll view, state of rendering engine, etc),
 * a range controller can choose which mode it should use at a given time.
 */
typedef NS_ENUM(NSInteger, ASLayoutRangeMode) {
  ASLayoutRangeModeUnspecified = -1,
  
  /**
   * Minimum mode is used when a range controller should limit the amount of work it performs.
   * Thus, fewer views/layers are created and less data is fetched, saving system resources.
   * Range controller can automatically switch to full mode when conditions change.
   */
  ASLayoutRangeModeMinimum = 0,
    
  /**
   * Normal/Full mode that a range controller uses to provide the best experience for end users.
   * This mode is usually used for an active scroll view.
   * A range controller under this requires more resources compare to minimum mode.
   */
  ASLayoutRangeModeFull,
  
  /**
   * Visible Only mode is used when a range controller should set its display and preload regions to only the size of their bounds.
   * This causes all additional backing stores & preloaded data to be released, while ensuring a user revisiting the view will
   * still be able to see the expected content.  This mode is automatically set on all ASRangeControllers when the app suspends,
   * allowing the operating system to keep the app alive longer and increase the chance it is still warm when the user returns.
   */
  ASLayoutRangeModeVisibleOnly,
  
  /**
   * Low Memory mode is used when a range controller should discard ALL graphics buffers, including for the area that would be visible
   * the next time the user views it (bounds).  The only range it preserves is Preload, which is limited to the bounds, allowing
   * the content to be restored relatively quickly by re-decoding images (the compressed images are ~10% the size of the decoded ones,
   * and text is a tiny fraction of its rendered size).
   */
  ASLayoutRangeModeLowMemory
};

处于不同状态时,预加载范围不同。如下所示:

+ (std::vector<std::vector<ASRangeTuningParameters>>)defaultTuningParameters
{
  auto tuningParameters = std::vector<std::vector<ASRangeTuningParameters>> (ASLayoutRangeModeCount, std::vector<ASRangeTuningParameters> (ASLayoutRangeTypeCount));

  tuningParameters[ASLayoutRangeModeFull][ASLayoutRangeTypeDisplay] = {
    .leadingBufferScreenfuls = 1.0,
    .trailingBufferScreenfuls = 0.5
  };

  tuningParameters[ASLayoutRangeModeFull][ASLayoutRangeTypePreload] = {
    .leadingBufferScreenfuls = 2.5,
    .trailingBufferScreenfuls = 1.5
  };

  tuningParameters[ASLayoutRangeModeMinimum][ASLayoutRangeTypeDisplay] = {
    .leadingBufferScreenfuls = 0.25,
    .trailingBufferScreenfuls = 0.25
  };
  tuningParameters[ASLayoutRangeModeMinimum][ASLayoutRangeTypePreload] = {
    .leadingBufferScreenfuls = 0.5,
    .trailingBufferScreenfuls = 0.25
  };

  tuningParameters[ASLayoutRangeModeVisibleOnly][ASLayoutRangeTypeDisplay] = {
    .leadingBufferScreenfuls = 0,
    .trailingBufferScreenfuls = 0
  };
  tuningParameters[ASLayoutRangeModeVisibleOnly][ASLayoutRangeTypePreload] = {
    .leadingBufferScreenfuls = 0,
    .trailingBufferScreenfuls = 0
  };

  // The Low Memory range mode has special handling. Because a zero range still includes the visible area / bounds,
  // in order to implement the behavior of releasing all graphics memory (backing stores), ASRangeController must check
  // for this range mode and use an empty set for displayIndexPaths rather than querying the ASLayoutController for the indexPaths.
  tuningParameters[ASLayoutRangeModeLowMemory][ASLayoutRangeTypeDisplay] = {
    .leadingBufferScreenfuls = 0,
    .trailingBufferScreenfuls = 0
  };
  tuningParameters[ASLayoutRangeModeLowMemory][ASLayoutRangeTypePreload] = {
    .leadingBufferScreenfuls = 0,
    .trailingBufferScreenfuls = 0
  };
  return tuningParameters;
}

智能预加载支持多种方向。

界面状态回调

当滚动视图时,node 在范围内移动,并通过加载数据、渲染等方式做出适当反应。你的 node 子类可以通过实现相应的回调方法利用此机制。

// Visible Range
-didEnterVisibleState
-didExistVisibleState

// Display Range
-didEnterDisplayState
-didExitDisplayState

// Preload Range
-didEnterPreloadState
-didExitPreloadState

在实现这些回调方法时,需要先调用super

节点容器 Node Container

在 node container 中使用 node

强烈推荐在 Texture 的 node container 中使用 node。Texture 提供以下容器:

Texture Node Container 对应UIKit控件
ASCollectionNode 代替UIKit中的UICollectionView
ASPagerNode 代替UIKit中的UIPageViewController
ASTableNode 代替UIKit中的UITableView
ASViewController 代替UIKit中的UIViewController
ASNavigationController 代替UIKit中的UINavigationController,实现了ASVisibility协议。
ASTabBarController 代替UIKit中的UITabBarController,实现了ASVisiblity协议。

Texture 容器 Node Containers这篇文章会详细介绍。

使用 node container 的好处

Node container 自动管理其 node 的智能预加载。这意味着所有 node 的布局计算、数据获取、解码和渲染将异步完成。这也是推荐在 node container 中使用 node 的原因。

也可以直接使用 node,但除非有显式调用,否则 node 会在出现到屏幕上时才开始渲染(与UIKit一样),这会导致性能下降和内容闪烁。

节点子类 Node Subclasses

使用 node 而非UIKit组件的关键优势是所有 node 的布局、渲染均可在非主线程渲染,因此主线程可用于响应用户交互事件。

Texture 提供以下 node:

Texture Node 对应UIKit控件
ASDisplayNode 代替UIKit中的UIView,也是 Texture 的根 node。
所有node 均继承自ASDisplayNode
ASCellNode 代替UITableViewCellUICollectionViewCell
ASCellNode被用在ASTableNodeASCollectionNodeASPagerNode
ASScrollNode 代替UIKit中的UIScrollView
该 node 用于创建包含其他 node 的可滚动区域。
ASEditableTextNode
ASTextNode
代替UIKitUITextView
代替UIKitUILabel
ASImageNode
ASNetworkImageNode
ASMultiplexImageNode
代替UIKitUIImageView
ASVideoNode
ASVideoPlayerNode
代替AVFoundationAVPlayerLayer
ASControlNode 代替UIKitUIControl
ASButtonNode 代替UIKitUIButton
ASMapNode 代替MKMapViewMKMapView

尽管 node 与这些UIKit组件等效,但通常 Texture 的 node 提供了更高级的功能和便捷性。例如,ASNetworkImageNode会执行自动加载和缓存管理,还支持渐进式 jpeg 和 GIF。

Texture 文档中的AsyncDisplayKitOverview包含了上述 node 的基本实现。

Node 继承层级如下:

TextureInheritanceHierarchy

蓝色显示的 node 是对UIKit对应控件的封装。例如,ASScrollNode封装了UIScrollViewASCollectionNode封装了UICollectionView

Texture 基本控件 Node这篇文章会详细介绍上述 node。

子类化

创建子类时最重要的区别是创建ASViewController还是创建ASDisplayNode。这听起来很明显,但由于其差异很微妙,因此请务必牢记这一点。

ASDisplayNode

虽然子类化 node 类似于子类化UIView,但需要遵循一些准则,以确保充分发挥框架潜能,并确保节点的行为符合预期。

-init

使用 nodeBlocks 时,在后台线程上调用此方法。但由于 -init 方法完成前不能执行其他方法,因此在此方法中永远不需要使用锁。

要记住的最重要的事情是 -init 方法必须能够在任何队列上被调用。这意味着绝不能初始化任何UIKit对象、接触 node 的 view 或 layer。例如:node.layer.Xnode.view.X。也不能添加任何手势识别器。这些应在 -didLoad 方法中执行。

-didLoad

此方法在概念上类似于UIViewController的 -viewDidLoad,只调用一次,且在 backing view 加载后调用。该方法在主线程中调用,适合于执行对UIKit的操作。例如:添加手势识别器,触摸视图、图层,初始化UIKit对象。

-layoutSpecThatFits:

此方法指定视图布局,并在后台线程进行计算。在此方法中构建布局规范对象,该对象将产生 node 及子 node 大小和位置。应将大部分布局代码放到 -layoutSpecThatFits 方法中。

在此方法返回前,布局规范对象可以延展(malleable up)。返回后,将不可变。不要缓存布局规范,应在每次需要时重新创建布局规范。

该方法在后台线程运行,不要在此方法内设置任何node.viewnode.layer属性。另外,除非您知道自己在做什么,否则,不要在此方法中创建 node。-layoutSpecThatFits: 方法无需以调用 super 开始,这一点与其他方法不同。

-layout

在此方法中调用 super 时会将布局方案应用起来。在此方法中调用 super 之后,将立即计算布局,并测量、布局所有 subnode。

layout方法与UIViewController中的viewWillLayoutSubview方法类似。这是设置隐藏属性、设置基于视图的属性(非可布局属性)或设置背景颜色的好地方。可以将背景颜色设置放在layoutSpecThatFits:中,但可能存在时间问题。如果在使用UIView,则可以在此设置其 frame。另外,始终可以使用initWithViewBlock:创建 node,然后在其他位置的后台线程上调整其大小。

layout方法在主线程中调用。如果在用 layout spec,应避免依赖此方法,因为将布局移到后台线程有利于提高性能。少于十分之一的子类需要此方法。

layout方法的一个最佳用途是您希望 node 为精准大小。例如,当希望 collectionNode 占据全屏时,布局规范不能很好的支持这种情况,这时最好的方法是在layout方法中手动设置其 frame。

subnode.frame = self.bounds;

如果希望在ASViewController中实现同样效果,需在viewWillLayoutSubviews方法中实现。如果 node 通过initWithNode:方法创建,则会自动实现上述效果。

ASViewController

ASViewControllerUIViewController的子类,拥有管理 node 的特殊功能。由于ASViewControllerUIViewController的子类,因此,所有方法都在主线程上调用,且始终在主线程上创建UIViewController

-init

init方法在ASViewController生命周期最开始调用,且只会调用一次。和UIViewController的初始化类似,最佳实践是永远不要在此方法内访问self.viewself.node.view,因为这样将强制尽早创建视图。应在viewDidLoad中操作视图。

ASViewController的指定初始化程序是initWithNode:,常见初始化方法如下所示:

- (instancetype)init {
    _pagerNode = [[ASPagerNode alloc] init];
    self = [super initWithNode:_pagerNode];
    
    // setup any instance variables or properties here
    if (self) {
        _pagerNode.dataSource = self;
        _pagerNode.delegate = self;
    }
    return self;
}

注意在调用 super 之前,ASViewController是如何初始化的。ASViewController管理 node 的方式与UIViewController管理view类似,但初始化方式略有不同。

-loadView

viewDidLoad相比没有优势,因此不推荐使用该方法。只要不为self.view赋值即可安全使用该方法。调用[super loadView]方法会将其设置到 node.view。

-viewDidLoad

在调用loadView方法后会调用viewDidLoad方法,在ASViewController的生命周期中viewDidLoad方法只调用一次。viewDidLoad方法是最早能够访问 load.view 的方法。在这里放置只需运行一次且需要访问 view、layer 的代码。例如,添加手势识别器。

布局代码永远不要放在此方法中,因为几何图形(geometry)更改时不会再次调用viewDidLoad方法,这一点同样适用于UIViewController。即使目前不会有几何图形变化,也不建议在viewDidLoad中布局代码。

-viewWillLayoutSubviews

viewWillLayoutSubviews方法与 node 的layout方法调用时机一致,在ASViewController的生命周期中可能被调用多次。当ASViewController的边界(包括旋转、分屏,弹出键盘)、视图层级改变(包括添加、移除子视图,更改子视图大小)时会调用viewWillLayoutSubviews方法。

为了保持一致性,最佳做法是将所有布局代码放入此方法中。viewWillLayoutSubviews方法调用频率不高,不依赖视图大小的布局也可以放到这里。

-viewWillAppear: -viewDidDisappear:

ASViewController的 node 出现在屏幕上之前(最早可见)和刚从视图层级结构中移除之后(不再可见的最早时间)被调用。这些方法提供了操控控制器 present、dismiss 开始、停止动画的时机。也是用来记录用户操作的地方。

尽管这些方法会被调用多次,且几何信息已经存在,但不会在所有几何信息改变时调用该方法,因此不应将其用于核心布局代码。

下一篇:Texture 布局 Layout

Clone this wiki locally