NSOperation 的依赖关系(Dependencies)是其相对于 GCD 最具竞争力的特性。它允许我们将复杂的业务逻辑拆解为多个独立的原子操作,并像“搭积木”一样将它们连接起来。
其内部实现主要依赖于 KVO(Key-Value Observing)机制、状态机控制以及一个内部依赖表。
1. 核心模型:有向无环图 (DAG)
在数据结构层面,当你为多个 Operation 添加依赖时,你在底层实际上构建了一个有向无环图 (Directed Acyclic Graph) 。
- 节点 (Nodes) :每一个
NSOperation实例。 - 边 (Edges) :
addDependency:方法建立的指向关系。 - 规则:调度器(OperationQueue)会遍历这张图,只有当一个节点的所有“入度”(即前置依赖)都消失时,该节点才会被激活。
2. 状态驱动:isReady 属性
NSOperation 内部有一个名为 isReady 的布尔属性。这是决定任务能否被 OperationQueue 调度执行的唯一关卡。
依赖检查的逻辑:
- 添加依赖:当你执行
opB.addDependency(opA)时,opB会将opA加入其内部的一个私有集合中。 - KVO 监听:
opB会通过 KVO 监听opA的isFinished属性。 - 状态变更:当
opA完成任务(无论是因为执行完毕还是被取消),它的isFinished变为YES。 - 通知响应:
opB收到 KVO 通知后,会检查自己所有依赖的任务。如果全部都处于isFinished状态,opB就会通过 KVO 发出通知,告知外界自己的isReady变为YES。
3. 跨队列依赖 (Cross-Queue Dependencies)
这是 NSOperation 最强大的地方之一:依赖关系可以跨越不同的 OperationQueue。
- 原理:因为依赖机制是基于对象本身的状态(
isFinished)和 KVO,而不是由特定的队列管理的。 - 场景:你可以在
后台队列中放一个“解压文件”的任务,在主线程队列中放一个“弹出提示框”的任务。只需让 UI 任务依赖于后台任务,UI 任务就会在后台任务完成的一瞬间自动被主线程调度。
4. 源码级的关键行为
- 避免死锁:
NSOperation内部会检测循环依赖(例如 A 依赖 B,B 依赖 A)。虽然系统不一定会抛出崩溃,但会导致这两个任务永远处于等待状态。 - 内存释放:一旦一个任务完成,其依赖的任务列表通常会被清理,以释放对前置任务对象的强引用。
- 取消传播:注意,取消操作不会自动沿着依赖链传播。如果
opA被取消了,它会立即进入isFinished状态,从而触发opB开始执行。如果opB也需要停止,你必须手动处理。
5. 依赖关系 vs GCD 组
| 特性 | NSOperation 依赖 | GCD Dispatch Group |
|---|---|---|
| 逻辑结构 | 1 对 1 或 1 对多,精确链接 | 多对 1,侧重“汇总” |
| 动态性 | 运行期间可动态添加依赖 | 必须在启动前加入组 |
| 灵活性 | 支持跨队列、跨线程依赖 | 仅限加入同一个 Group 的任务 |
💡 深度洞察:如何构建安全的依赖链?
在实际开发中,如果 opB 依赖 opA 是为了获取 opA 处理后的数据,你需要确保在 opB 开始执行的第一行代码中,通过 self.dependencies 找到 opA 并安全地取出结果。