这是 Texture 文档系列翻译,其中结合了自己的理解和工作中的使用体会。如果哪里有误,希望指出。
Texture是一个UI框架,最初诞生于Facebook的Paper app。通过使 UI 相关操作线程安全来优化app,这意味着能够将所有昂贵视图相关操作转移到后台线程,在显示前进行前期准备。
Texture之前名称为AsyncDisplayKit。
Texture 的基本单位是node
。ASDisplayNode
是对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 添加到滚动或翻页视图时,其通常处于以下状态之一。这意味着,随着滚动视图的滚动,它们的界面状态将随着它们的移动而更新。
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 |
代替UITableViewCell 、UICollectionViewCell 。ASCellNode 被用在ASTableNode 、ASCollectionNode 、ASPagerNode 。 |
ASScrollNode |
代替UIKit 中的UIScrollView 。该 node 用于创建包含其他 node 的可滚动区域。 |
ASEditableTextNode ASTextNode
|
代替UIKit 的UITextView 。代替 UIKit 的UILabel 。 |
ASImageNode ASNetworkImageNode ASMultiplexImageNode
|
代替UIKit 的UIImageView 。 |
ASVideoNode ASVideoPlayerNode
|
代替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
。这听起来很明显,但由于其差异很微妙,因此请务必牢记这一点。
ASDisplayNode
虽然子类化 node 类似于子类化UIView
,但需要遵循一些准则,以确保充分发挥框架潜能,并确保节点的行为符合预期。
-init
使用 nodeBlocks 时,在后台线程上调用此方法。但由于 -init 方法完成前不能执行其他方法,因此在此方法中永远不需要使用锁。
要记住的最重要的事情是 -init 方法必须能够在任何队列上被调用。这意味着绝不能初始化任何UIKit
对象、接触 node 的 view 或 layer。例如:node.layer.X
或node.view.X
。也不能添加任何手势识别器。这些应在 -didLoad 方法中执行。
-didLoad
此方法在概念上类似于UIViewController
的 -viewDidLoad,只调用一次,且在 backing view 加载后调用。该方法在主线程中调用,适合于执行对UIKit
的操作。例如:添加手势识别器,触摸视图、图层,初始化UIKit
对象。
-layoutSpecThatFits:
此方法指定视图布局,并在后台线程进行计算。在此方法中构建布局规范对象,该对象将产生 node 及子 node 大小和位置。应将大部分布局代码放到 -layoutSpecThatFits 方法中。
在此方法返回前,布局规范对象可以延展(malleable up)。返回后,将不可变。不要缓存布局规范,应在每次需要时重新创建布局规范。
该方法在后台线程运行,不要在此方法内设置任何node.view
或node.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
ASViewController
是UIViewController
的子类,拥有管理 node 的特殊功能。由于ASViewController
是UIViewController
的子类,因此,所有方法都在主线程上调用,且始终在主线程上创建UIViewController
。
-init
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
类似,但初始化方式略有不同。
-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 开始、停止动画的时机。也是用来记录用户操作的地方。
尽管这些方法会被调用多次,且几何信息已经存在,但不会在所有几何信息改变时调用该方法,因此不应将其用于核心布局代码。
欢迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/Texture%20%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5.md