-
Notifications
You must be signed in to change notification settings - Fork 210
Texture 核心概念
这是 Texture 文档系列翻译,其中结合了自己的理解和工作中的使用体会。如果哪里有误,希望指出。
Texture是一个UI框架,最初诞生于Facebook的Paper app。通过使 UI 相关操作线程安全来优化app,这意味着能够将所有昂贵视图相关操作转移到后台线程,在显示前进行前期准备。
Texture之前名称为AsyncDisplayKit。
Texture 的基本单位是node。ASDisplayNode是对UIView的抽象,进而也是对CALayer的抽象。node是线程安全的,因此可以在后台并行处理。
为了保持用户界面流畅和响应速度,app需要以60帧 (frame) 每秒速度绘制,即主线程需要在十六分之一秒绘制一帧,布局和绘制需要在16毫秒内完成,而且由于系统开销,代码通常只有不到十毫秒运行时间,超过这个时间就会导致掉帧。
Texture 使您可以将图片解码、文本大小计算和渲染,以及其他昂贵的 UI 操作从主线程中移除,以保持主线程可响应用户交互。
可以通过 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 会出现闪烁。
将 node 添加到滚动或翻页视图时,其通常处于以下状态之一。这意味着,随着滚动视图的滚动,它们的界面状态将随着它们的移动而更新。

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。
强烈推荐在 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 的智能预加载。这意味着所有 node 的布局计算、数据获取、解码和渲染将异步完成。这也是推荐在 node container 中使用 node 的原因。
也可以直接使用 node,但除非有显式调用,否则 node 会在出现到屏幕上时才开始渲染(与UIKit一样),这会导致性能下降和内容闪烁。
使用 node 而非UIKit组件的关键优势是所有 node 的布局、渲染均可在非主线程渲染,因此主线程可用于响应用户交互事件。
Texture 提供以下 node:
| Texture Node | 对应UIKit控件 |
|---|---|
ASDisplayNode |
代替UIKit中的UIView,也是 Texture 的根 node。所有node 均继承自 ASDisplayNode。 |
ASCellNode |
代替UITableViewCell、UICollectionViewCell。ASCellNode被用在ASTableNode、ASCollectionNode、ASPagerNode。 |
ASScrollNode |
代替UIKit中的UIScrollView。该 node 用于创建包含其他 node 的可滚动区域。 |
ASEditableTextNodeASTextNode
|
代替UIKit的UITextView。代替 UIKit的UILabel。 |
ASImageNodeASNetworkImageNodeASMultiplexImageNode
|
代替UIKit的UIImageView。 |
ASVideoNodeASVideoPlayerNode
|
代替AVFoundation的AVPlayerLayer。 |
ASControlNode |
代替UIKit的UIControl。 |
ASButtonNode |
代替UIKit的UIButton。 |
ASMapNode |
代替MKMapView的MKMapView。 |
尽管 node 与这些UIKit组件等效,但通常 Texture 的 node 提供了更高级的功能和便捷性。例如,ASNetworkImageNode会执行自动加载和缓存管理,还支持渐进式 jpeg 和 GIF。
Texture 文档中的AsyncDisplayKitOverview包含了上述 node 的基本实现。
Node 继承层级如下:

蓝色显示的 node 是对UIKit对应控件的封装。例如,ASScrollNode封装了UIScrollView,ASCollectionNode封装了UICollectionView。
Texture 基本控件 Node这篇文章会详细介绍上述 node。
创建子类时最重要的区别是创建ASViewController还是创建ASDisplayNode。这听起来很明显,但由于其差异很微妙,因此请务必牢记这一点。
虽然子类化 node 类似于子类化UIView,但需要遵循一些准则,以确保充分发挥框架潜能,并确保节点的行为符合预期。
使用 nodeBlocks 时,在后台线程上调用此方法。但由于 -init 方法完成前不能执行其他方法,因此在此方法中永远不需要使用锁。
要记住的最重要的事情是 -init 方法必须能够在任何队列上被调用。这意味着绝不能初始化任何UIKit对象、接触 node 的 view 或 layer。例如:node.layer.X或node.view.X。也不能添加任何手势识别器。这些应在 -didLoad 方法中执行。
此方法在概念上类似于UIViewController的 -viewDidLoad,只调用一次,且在 backing view 加载后调用。该方法在主线程中调用,适合于执行对UIKit的操作。例如:添加手势识别器,触摸视图、图层,初始化UIKit对象。
-layoutSpecThatFits:
此方法指定视图布局,并在后台线程进行计算。在此方法中构建布局规范对象,该对象将产生 node 及子 node 大小和位置。应将大部分布局代码放到 -layoutSpecThatFits 方法中。
在此方法返回前,布局规范对象可以延展(malleable up)。返回后,将不可变。不要缓存布局规范,应在每次需要时重新创建布局规范。
该方法在后台线程运行,不要在此方法内设置任何node.view或node.layer属性。另外,除非您知道自己在做什么,否则,不要在此方法中创建 node。-layoutSpecThatFits: 方法无需以调用 super 开始,这一点与其他方法不同。
在此方法中调用 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是UIViewController的子类,拥有管理 node 的特殊功能。由于ASViewController是UIViewController的子类,因此,所有方法都在主线程上调用,且始终在主线程上创建UIViewController。
init方法在ASViewController生命周期最开始调用,且只会调用一次。和UIViewController的初始化类似,最佳实践是永远不要在此方法内访问self.view或self.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类似,但初始化方式略有不同。
与viewDidLoad相比没有优势,因此不推荐使用该方法。只要不为self.view赋值即可安全使用该方法。调用[super loadView]方法会将其设置到 node.view。
在调用loadView方法后会调用viewDidLoad方法,在ASViewController的生命周期中viewDidLoad方法只调用一次。viewDidLoad方法是最早能够访问 load.view 的方法。在这里放置只需运行一次且需要访问 view、layer 的代码。例如,添加手势识别器。
布局代码永远不要放在此方法中,因为几何图形(geometry)更改时不会再次调用viewDidLoad方法,这一点同样适用于UIViewController。即使目前不会有几何图形变化,也不建议在viewDidLoad中布局代码。
viewWillLayoutSubviews方法与 node 的layout方法调用时机一致,在ASViewController的生命周期中可能被调用多次。当ASViewController的边界(包括旋转、分屏,弹出键盘)、视图层级改变(包括添加、移除子视图,更改子视图大小)时会调用viewWillLayoutSubviews方法。
为了保持一致性,最佳做法是将所有布局代码放入此方法中。viewWillLayoutSubviews方法调用频率不高,不依赖视图大小的布局也可以放到这里。
ASViewController的 node 出现在屏幕上之前(最早可见)和刚从视图层级结构中移除之后(不再可见的最早时间)被调用。这些方法提供了操控控制器 present、dismiss 开始、停止动画的时机。也是用来记录用户操作的地方。
尽管这些方法会被调用多次,且几何信息已经存在,但不会在所有几何信息改变时调用该方法,因此不应将其用于核心布局代码。