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.viewnode.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 的常见陷阱,写出高性能的代码! 🚀

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


作者:[你的昵称]

链接:[文章链接]

来源:稀土掘金

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

相关推荐
2501_916008891 小时前
Python抓包HTTPS详解:Wireshark、Fiddler、Charles等工具使用教程
python·ios·小程序·https·uni-app·wireshark·iphone
Sheffi661 小时前
iOS Block 底层结构与变量捕获原理深度解析
ios
2501_916008892 小时前
uni-app 上架到 App Store 的项目流程,构建、打包与使用开心上架(Appuploader)上传
android·ios·小程序·https·uni-app·iphone·webview
00后程序员张2 小时前
怎么在 iOS 上架 App,从构建端到审核端的全流程协作解析
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_915918412 小时前
iOS 开发者工具全景指南,构建高效开发、调试与性能优化的多工具工作体系
android·ios·性能优化·小程序·uni-app·iphone·webview
wjm0410063 小时前
秋招ios面试 -- 真题篇(一)
开发语言·ios·面试
微:xsooop19 小时前
iOS 上架4.3a 审核4.3a 被拒4.3a 【灾难来袭】
flutter·unity·ios·uniapp
Haha_bj21 小时前
iOS深入理解事件传递及响应
前端·ios·app
在下历飞雨1 天前
Kuikly基础之动画实战:让孤寡青蛙“活”过来
前端·ios·harmonyos