Textture 生命周期

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.view or node.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 完全可见 出现在屏幕内 播放动画/视频

📖 官方文档引用

来自 Intelligent Preloading:

"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() - 资源释放

🧵 线程特性

✅ 主线程执行

📖 官方文档引用

来自 Texture Best Practices:

"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() {}
}

📚 官方资源

  1. 源码注释 :ASDisplayNode.h
  2. 官方文档 :Node Lifecycle
  3. 智能预加载 :Intelligent Preloading
  4. 线程安全指南 :Thread Safety
  5. 容器节点文档 :ASViewController

🎓 进阶主题

Interface State 的精确控制

你可以通过 interfaceState 属性手动检查节点状态:

swift 复制代码
if interfaceState.contains(.visible) {
    print("节点当前可见")
}

if interfaceState.contains(.preload) {
    print("节点在预加载范围内")
}

自定义预加载距离

swift 复制代码
// 在容器节点(如 ASTableNode)中设置
tableNode.leadingScreensForBatching = 2.0  // 提前 2 屏开始预加载

性能优化技巧

  1. 合理使用 Interface State

    • Preload: 网络请求(距离远,提前加载)
    • Display: 文本渲染(即将显示)
    • Visible: 动画/视频(只在可见时播放)
  2. 及时释放资源

swift 复制代码
   override func didExitVisibleState() {
       super.didExitVisibleState()
       videoNode.pause()  // 立即暂停,节省电量
   }
   
   override func clearContents() {
       super.clearContents()
       imageNode.image = nil  // 释放内存
   }
  1. 避免在 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()
   }

💡 总结

三条黄金法则

  1. 线程安全第一

    • init() 和 layoutSpecThatFits() 可能在后台线程
    • 绝对不要在这两个方法中访问 UIKit
  2. 职责分离

    • init: 创建节点树
    • didLoad: UIKit 交互
    • layoutSpec: 纯布局描述
    • Interface State: 生命周期响应
  3. 性能优先

    • 使用 Preload 提前加载
    • 使用 clearContents 释放资源
    • 及时停止动画和视频

调试技巧

开启 Texture 的调试日志:

swift 复制代码
// 在 AppDelegate 中
#if DEBUG
ASDisplayNode.shouldShowRangeDebugOverlay = true
#endif

这会在屏幕上显示每个节点的 Interface State,帮助你理解生命周期。


希望这份指南能帮助你避开 Texture 的常见陷阱,写出高性能的代码! 🚀

如有问题欢迎评论讨论,也欢迎补充更多实战经验!


作者:你的昵称

链接:文章链接

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关推荐
茶底世界之下4 天前
为什么预览、录制、离线导出不能共享同一个背压策略?
ios·swift
2501_916007474 天前
苹果商店iOS应用上架费用全面解析:开发者计划与审核费用计算
android·ios·小程序·https·uni-app·iphone·webview
2501_916008894 天前
如何实现iOS App代码混淆:SwiftShield使用指南
android·ios·小程序·https·uni-app·iphone·webview
2501_915909064 天前
移动端开发工具链怎么组合比较好,iOS、Android、跨端三套配置方案
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
2501_916008894 天前
怎么用 Godot 导出 iOS 应用并上架 App Store?签名字段该填什么?
android·ios·小程序·https·uni-app·iphone·webview
2501_915918415 天前
iOS编程用什么软件好?主流工具Xcode、AppCode、CodeRunner与新兴快蝎对比
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
终端安全笔记5 天前
iOS 27 之后「策略空转」:设备升级不报错,但旧策略不再管它
android·网络·安全·ios·智能手机
9765033355 天前
iOS 上架 4.3a 被拒【uniapp专讲】
flutter·ios·objective-c·uniapp·swift
黑化旺仔6 天前
【OC】KVO
macos·ios·objective-c·cocoa
HouWan6 天前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple