Texture (AsyncDisplayKit) 节点生命周期完全指南
本文详解 Texture 框架中
ASDisplayNode的完整生命周期,包括线程安全陷阱和官方最佳实践。
📋 生命周期流程图
scss
1. init() // 节点创建(⚠️ 可能在后台线程)
↓
2. didLoad() // view/layer 已创建(✅ 主线程)
↓
3. layoutSpecThatFits(_:) // 布局测量(⚠️ 可能在后台线程)
↓
4. didEnterPreloadState() // 即将接近屏幕(✅ 主线程)
↓
5. didEnterDisplayState() // 即将显示(✅ 主线程)
↓
6. didEnterVisibleState() // 完全可见(✅ 主线程)
↓
7. didExitVisibleState() // 离开可见区(✅ 主线程)
↓
8. didExitDisplayState() // 离开显示区(✅ 主线程)
↓
9. didExitPreloadState() // 离开预加载区(✅ 主线程)
↓
10. clearContents() // 释放资源(✅ 主线程)
↓
11. deinit // 节点销毁
1️⃣ init() - 节点初始化
🧵 线程特性
⚠️ 关键:可能在主线程或后台线程执行!
根据创建方式不同:
- 直接创建 :
let node = MyNode()→ 在调用线程执行 - Block 创建 :
ASCellNode { MyNode() }→ 在后台线程执行
📖 官方文档引用
来自 ASDisplayNode.h:
"This method can be called on a background thread .
You MUST ensure that no UIKit objects are accessed."
✅ 应该做的事
swift
override init() {
super.init()
// ✅ 初始化子节点
let avatarNode = ASNetworkImageNode()
let textNode = ASTextNode()
// ✅ 设置 Node 层级属性(线程安全)
avatarNode.cornerRadius = 18
avatarNode.style.preferredSize = CGSize(width: 36, height: 36)
textNode.maximumNumberOfLines = 0
textNode.truncationMode = .byWordWrapping
// ✅ 添加子节点
automaticallyManagesSubnodes = true
addSubnode(avatarNode)
addSubnode(textNode)
}
❌ 绝对禁止的操作
swift
override init() {
super.init()
// ❌ 访问 view/layer(后台线程会崩溃)
self.view.backgroundColor = .white // 💥 Crash!
imageNode.view.layer.cornerRadius = 10 // 💥 Crash!
// ❌ 创建 UIKit 对象
let image = UIImage(named: "icon") // ⚠️ 可能有问题
// ❌ 调用 UIKit API
let color = UIColor.red.cgColor // ⚠️ 不推荐
}
🔧 替代方案对比
| 需求 | ❌ 错误写法 (init) | ✅ 正确写法 |
|---|---|---|
| 设置圆角 | node.view.layer.cornerRadius = 10 |
node.cornerRadius = 10 |
| 设置背景色 | node.view.backgroundColor = .red |
node.backgroundColor = .red |
| 添加手势 | node.view.addGestureRecognizer(...) |
在 didLoad() 中添加 |
| 设置阴影 | node.view.layer.shadowRadius = 5 |
在 didLoad() 中设置 |
2️⃣ didLoad() - 视图已创建
🧵 线程特性
✅ 始终在主线程执行
📖 官方文档引用
"Called on the main thread after the node's view or layer has been created.
This is the earliest time to safely access
node.viewornode.layer."
触发时机
当 node.view 或 node.layer 首次被访问时触发(懒加载),而不是创建后自动触发。
✅ 应该做的事
swift
override func didLoad() {
super.didLoad()
// ✅ 访问 UIKit 视图层级
textNode.view.isUserInteractionEnabled = true
// ✅ 添加手势
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tapGesture)
// ✅ 设置 layer 属性
imageNode.view.layer.shadowColor = UIColor.black.cgColor
imageNode.view.layer.shadowOffset = CGSize(width: 0, height: 2)
imageNode.view.layer.shadowRadius = 4
imageNode.view.layer.shadowOpacity = 0.3
// ✅ 设置 delegate
scrollNode.view.delegate = self
}
❌ 不应该做的事
swift
override func didLoad() {
super.didLoad()
// ❌ 不要做布局计算(应该在 layoutSpecThatFits 中)
textNode.frame = CGRect(x: 10, y: 10, width: 200, height: 40)
// ❌ 不要做数据加载(应该在 didEnterPreloadState 中)
fetchRemoteData()
}
3️⃣ layoutSpecThatFits(_:) - 布局测量
🧵 线程特性
⚠️ 可能在主线程或后台线程执行
根据调用场景:
- 异步测量(滚动时)→ 后台线程
- 同步布局(主动调用)→ 主线程
📖 官方文档引用
来自 ASDisplayNode.h:
"This method is called off the main thread .
It is called on the main thread only when being used synchronously .
Node subclasses should NEVER access their view or layer properties."
✅ 应该做的事
swift
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
// ✅ 设置 Node 的布局属性
avatarNode.style.preferredSize = CGSize(width: 36, height: 36)
// ✅ 创建布局规范
let textInset = ASInsetLayoutSpec(
insets: UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10),
child: textNode
)
let hStack = ASStackLayoutSpec.horizontal()
hStack.spacing = 8
hStack.alignItems = .start
hStack.children = [avatarNode, textInset]
return ASInsetLayoutSpec(
insets: UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12),
child: hStack
)
}
❌ 绝对禁止的操作
swift
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
// ❌ 访问 view(后台线程会崩溃)
self.view.backgroundColor = .white // 💥 Crash!
textNode.view.frame = CGRect(...) // 💥 Crash!
// ❌ 调用 UIKit API
let color = UIColor.red // ⚠️ 危险
// ❌ 修改状态变量(可能导致线程竞争)
self.isLoading = false // ⚠️ 需要加锁
return ASLayoutSpec()
}
💡 关键原则
layoutSpecThatFits 是纯函数风格:
- 输入:size 约束
- 输出:布局描述(ASLayoutSpec)
- 禁止:访问 UIKit、修改状态、产生副作用
4️⃣ Interface State 系统
Texture 提供了三级渐进式状态管理:
| 状态 | 距离屏幕 | 触发时机 | 典型用途 |
|---|---|---|---|
| Preload | 1-2 屏 | 即将进入可视区 | 网络请求、解码图片 |
| Display | 即将显示 | 准备渲染 | 文本光栅化、layer 内容 |
| Visible | 完全可见 | 出现在屏幕内 | 播放动画/视频 |
📖 官方文档引用
"Texture provides granular callbacks for when content enters different stages of the pipeline.
Use Preload for network fetching, Display for rendering, and Visible for animations."
完整示例
swift
class VideoCardNode: ASDisplayNode {
private let videoNode = ASVideoNode()
private let titleNode = ASTextNode()
// Preload: 距离屏幕 1-2 屏时触发
override func didEnterPreloadState() {
super.didEnterPreloadState()
// ✅ 开始网络请求
fetchVideoMetadata()
// ✅ 预加载视频
videoNode.asset = AVAsset(url: videoURL)
}
// Display: 即将显示但还没完全可见
override func didEnterDisplayState() {
super.didEnterDisplayState()
// ✅ 确保内容已渲染
print("内容已准备好显示")
}
// Visible: 完全出现在屏幕内
override func didEnterVisibleState() {
super.didEnterVisibleState()
// ✅ 启动动画
startPulseAnimation()
// ✅ 播放视频
videoNode.play()
// ✅ 曝光打点
Analytics.trackImpression(itemId: videoId)
}
// 退出可见状态
override func didExitVisibleState() {
super.didExitVisibleState()
// ✅ 立即停止动画
stopPulseAnimation()
// ✅ 暂停视频
videoNode.pause()
}
// 退出显示状态
override func didExitDisplayState() {
super.didExitDisplayState()
print("已退出显示区域")
}
// 退出预加载状态
override func didExitPreloadState() {
super.didExitPreloadState()
// ✅ 取消网络请求
cancelNetworkTasks()
}
}
🗑️ clearContents() - 资源释放
🧵 线程特性
✅ 主线程执行
📖 官方文档引用
"Override to clear any cached or calculated content when the node is no longer visible.
This is called automatically by the framework to manage memory."
触发时机
节点完全退出预加载范围后,系统自动调用
✅ 应该做的事
swift
override func clearContents() {
super.clearContents()
// ✅ 释放大图(可重建的资源)
imageNode.image = nil
// ✅ 停止动画
lottieAnimationNode.stop()
lottieAnimationNode.animationView = nil
// ✅ 清除缓存数据
cachedRenderData = nil
// ✅ 释放视频资源
videoNode.asset = nil
}
⚠️ 注意事项
clearContents()会在节点完全退出预加载范围后自动调用- 不需要手动调用
- 主要用于释放可重建的资源(如解码后的图片、渲染缓存)
- 不要释放配置数据(如 URL、ID、样式设置等)
🎯 最佳实践总结
线程安全检查表
| 生命周期方法 | 线程 | 可访问 view? | 可访问 UIKit? |
|---|---|---|---|
init() |
⚠️ 后台/主 | ❌ 否 | ❌ 否 |
didLoad() |
✅ 主线程 | ✅ 是 | ✅ 是 |
layoutSpecThatFits() |
⚠️ 后台/主 | ❌ 否 | ❌ 否 |
didEnter*State() |
✅ 主线程 | ✅ 是 | ✅ 是 |
didExit*State() |
✅ 主线程 | ✅ 是 | ✅ 是 |
clearContents() |
✅ 主线程 | ✅ 是 | ✅ 是 |
记忆口诀
"初建布局在后台,装载显示回主干"
- 初 (init) 建 (layoutSpec) 可能在后台线程 → 禁止访问 UIKit
- 装 (didLoad) 载显示 (Interface State) 都在主线程 → 可以访问 UIKit
职责分离原则
swift
class MyNode: ASDisplayNode {
// ✅ init: 创建节点树 + 设置静态属性
override init() {
super.init()
addSubnode(avatarNode)
avatarNode.cornerRadius = 10
}
// ✅ didLoad: UIKit 交互
override func didLoad() {
super.didLoad()
view.addGestureRecognizer(...)
}
// ✅ layoutSpec: 纯布局描述
override func layoutSpecThatFits(_ size: ASSizeRange) -> ASLayoutSpec {
return ASStackLayoutSpec(...)
}
// ✅ Preload: 数据加载
override func didEnterPreloadState() {
super.didEnterPreloadState()
fetchData()
}
// ✅ Visible: 动画/视频
override func didEnterVisibleState() {
super.didEnterVisibleState()
videoNode.play()
}
// ✅ clearContents: 释放可重建资源
override func clearContents() {
super.clearContents()
imageNode.image = nil
}
}
⚠️ 常见崩溃场景
场景 1:在 init 中访问 view
swift
// ❌ 当使用 ASCellNode(block:) 时会崩溃
override init() {
super.init()
self.view.backgroundColor = .white // 💥 后台线程访问 UIKit
}
解决方案:
swift
override init() {
super.init()
self.backgroundColor = .white // ✅ 使用 Node 的属性
}
override func didLoad() {
super.didLoad()
self.view.layer.shadowRadius = 5 // ✅ 在 didLoad 中访问 layer
}
场景 2:在 layoutSpec 中修改 view
swift
// ❌ 后台线程调用时会崩溃
override func layoutSpecThatFits(_ size: ASSizeRange) -> ASLayoutSpec {
textNode.view.numberOfLines = 2 // 💥 访问了 UILabel
return ASLayoutSpec()
}
解决方案:
swift
override func layoutSpecThatFits(_ size: ASSizeRange) -> ASLayoutSpec {
textNode.maximumNumberOfLines = 2 // ✅ 使用 Node 的属性
return ASLayoutSpec()
}
场景 3:在 layoutSpec 中调用 UIColor
swift
// ⚠️ 后台线程可能有问题
override func layoutSpecThatFits(_ size: ASSizeRange) -> ASLayoutSpec {
let color = UIColor.systemBlue // 危险!
textNode.backgroundColor = color
return ASLayoutSpec()
}
解决方案:
swift
// ✅ 在 init 中设置
override init() {
super.init()
textNode.backgroundColor = UIColor.systemBlue
}
// 或在 didLoad 中设置
override func didLoad() {
super.didLoad()
textNode.view.backgroundColor = UIColor.systemBlue
}
📊 完整生命周期示例
swift
class CompleteExampleNode: ASDisplayNode {
private let avatarNode = ASNetworkImageNode()
private let titleNode = ASTextNode()
private let videoNode = ASVideoNode()
// 1️⃣ 初始化(可能在后台线程)
override init() {
super.init()
print("✅ 1. init - 可能在后台线程")
// 只做线程安全操作
automaticallyManagesSubnodes = true
avatarNode.cornerRadius = 20
titleNode.maximumNumberOfLines = 2
}
// 2️⃣ 视图已创建(主线程)
override func didLoad() {
super.didLoad()
print("✅ 2. didLoad - 主线程")
// 访问 UIKit
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tapGesture)
videoNode.view.layer.cornerRadius = 8
}
// 3️⃣ 布局测量(可能在后台线程)
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
print("✅ 3. layoutSpecThatFits - 可能在后台线程")
// 只做布局描述
avatarNode.style.preferredSize = CGSize(width: 40, height: 40)
let vStack = ASStackLayoutSpec.vertical()
vStack.spacing = 8
vStack.children = [avatarNode, titleNode, videoNode]
return ASInsetLayoutSpec(insets: UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16), child: vStack)
}
// 4️⃣ 进入预加载状态(主线程)
override func didEnterPreloadState() {
super.didEnterPreloadState()
print("✅ 4. didEnterPreloadState - 主线程")
// 开始网络请求
fetchVideoData()
}
// 5️⃣ 进入显示状态(主线程)
override func didEnterDisplayState() {
super.didEnterDisplayState()
print("✅ 5. didEnterDisplayState - 主线程")
}
// 6️⃣ 进入可见状态(主线程)
override func didEnterVisibleState() {
super.didEnterVisibleState()
print("✅ 6. didEnterVisibleState - 主线程")
// 播放视频和动画
videoNode.play()
startAnimation()
}
// 7️⃣ 退出可见状态(主线程)
override func didExitVisibleState() {
super.didExitVisibleState()
print("✅ 7. didExitVisibleState - 主线程")
// 停止视频和动画
videoNode.pause()
stopAnimation()
}
// 8️⃣ 退出显示状态(主线程)
override func didExitDisplayState() {
super.didExitDisplayState()
print("✅ 8. didExitDisplayState - 主线程")
}
// 9️⃣ 退出预加载状态(主线程)
override func didExitPreloadState() {
super.didExitPreloadState()
print("✅ 9. didExitPreloadState - 主线程")
// 取消网络请求
cancelNetworkTasks()
}
// 🔟 清除内容(主线程)
override func clearContents() {
super.clearContents()
print("✅ 10. clearContents - 主线程")
// 释放资源
avatarNode.image = nil
videoNode.asset = nil
}
// 1️⃣1️⃣ 销毁
deinit {
print("✅ 11. deinit")
}
@objc private func handleTap() {
print("节点被点击")
}
private func fetchVideoData() {}
private func cancelNetworkTasks() {}
private func startAnimation() {}
private func stopAnimation() {}
}
📚 官方资源
- 源码注释 :ASDisplayNode.h
- 官方文档 :Node Lifecycle
- 智能预加载 :Intelligent Preloading
- 线程安全指南 :Thread Safety
- 容器节点文档 :ASViewController
🎓 进阶主题
Interface State 的精确控制
你可以通过 interfaceState 属性手动检查节点状态:
swift
if interfaceState.contains(.visible) {
print("节点当前可见")
}
if interfaceState.contains(.preload) {
print("节点在预加载范围内")
}
自定义预加载距离
swift
// 在容器节点(如 ASTableNode)中设置
tableNode.leadingScreensForBatching = 2.0 // 提前 2 屏开始预加载
性能优化技巧
-
合理使用 Interface State
- Preload: 网络请求(距离远,提前加载)
- Display: 文本渲染(即将显示)
- Visible: 动画/视频(只在可见时播放)
-
及时释放资源
swift
override func didExitVisibleState() {
super.didExitVisibleState()
videoNode.pause() // 立即暂停,节省电量
}
override func clearContents() {
super.clearContents()
imageNode.image = nil // 释放内存
}
- 避免在 layoutSpec 中做重计算
swift
// ❌ 每次布局都重新计算
override func layoutSpecThatFits(_ size: ASSizeRange) -> ASLayoutSpec {
let processedText = heavyTextProcessing(rawText) // 耗时操作
textNode.attributedText = processedText
return ASLayoutSpec()
}
// ✅ 在数据更新时计算一次
func updateData(_ newText: String) {
let processedText = heavyTextProcessing(newText)
textNode.attributedText = processedText
setNeedsLayout()
}
💡 总结
三条黄金法则
-
线程安全第一
init()和layoutSpecThatFits()可能在后台线程- 绝对不要在这两个方法中访问 UIKit
-
职责分离
- init: 创建节点树
- didLoad: UIKit 交互
- layoutSpec: 纯布局描述
- Interface State: 生命周期响应
-
性能优先
- 使用 Preload 提前加载
- 使用 clearContents 释放资源
- 及时停止动画和视频
调试技巧
开启 Texture 的调试日志:
swift
// 在 AppDelegate 中
#if DEBUG
ASDisplayNode.shouldShowRangeDebugOverlay = true
#endif
这会在屏幕上显示每个节点的 Interface State,帮助你理解生命周期。
希望这份指南能帮助你避开 Texture 的常见陷阱,写出高性能的代码! 🚀
如有问题欢迎评论讨论,也欢迎补充更多实战经验!
作者:[你的昵称]
链接:[文章链接]
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。