在处理复杂的 NSOperation 依赖时,系统虽然提供了强大的管理能力,但由于其基于 状态机 和 KVO 的特性,在极端场景下会出现一些隐蔽且致命的陷阱。
以下是四个最核心的“坑”:
1. 循环依赖导致的“静默死锁”
这是最经典但也最容易发生的错误。
- 现象:任务 A 依赖 B,B 依赖 A。
- 后果:
OperationQueue不会崩溃,也不会报错。这两个任务会永远处于isReady = NO的状态,静默地占用着内存。 - 坑点:在大型项目中,依赖关系可能跨越多个模块(A -> B -> C -> A),这种隐蔽的环状结构很难通过日志排查。
2. 依赖链中的“取消陷阱” (Cancellation Propagation)
这是开发者最常误解的地方:cancel 不会沿依赖链向下传递。
- 场景:任务 B 依赖 A。如果 A 因为出错被取消了。
- 陷阱:A 被取消后,会立即标记为
isFinished = YES。此时,OperationQueue认为 A 已经“顺利完成”,从而自动启动任务 B。 - 后果:如果 B 的逻辑依赖于 A 的输出数据,而 A 根本没执行,B 可能会因为访问空数据而崩溃或逻辑错误。
- 对策:在 B 的
main方法开头,必须检查if (self.dependencies.firstObject.isCancelled)。
3. 跨队列依赖引发的“线程饥饿”
NSOperation 支持跨队列依赖,但这引入了复杂的资源竞争。
- 陷阱:如果你在主队列(Main Queue)中有一个任务 A,它依赖于后台队列中一个优先级极低(Background QoS)的任务 B。
- 后果:由于主线程优先级极高,它会不断唤醒去检查 A 是否就绪,而 B 因为 QoS 过低抢不到 CPU 时间片。这可能导致主线程看起来像“卡死”了一样在等待后台任务。
- 优先级反转:虽然 iOS 有优先级提升机制,但在极高负载下,这种跨优先级的依赖依然会造成明显的 UI 掉帧。
4. 异步 Operation 的“假完成”
如果你自定义了 NSOperation 来封装异步任务(如网络请求),但没有正确重写 isAsynchronous 和状态管理:
- 坑点:默认的
Operation在main方法执行完最后一行代码后,就会自动把isFinished设为YES。 - 现象:对于异步网络请求,
main很快就跑完了,此时请求还在后台。OperationQueue认为该任务已完成,于是立即启动了依赖它的后续任务。 - 结果:后续任务拿不到还没回来的网络数据,导致逻辑崩盘。
5. 依赖链过长导致的内存堆积
由于 NSOperation 在执行完后,除非被从队列中彻底移除,否则它会一直持有它的 dependencies 列表。
- 陷阱:如果你构建了一个包含成千上万个任务的长链条,后面的任务会间接持有一大串已经执行完的
Operation实例。 - 后果:如果每个
Operation内部还持有了较大的数据(如图片),内存占用会线性飙升,直到整条链跑完。
总结:避坑指南
| 坑位 | 避坑方案 |
|---|---|
| 循环依赖 | 使用工具类或单元测试检测依赖图是否存在环。 |
| 取消传播 | 在子任务开始前,务必检查 dependencies 的 isCancelled 状态。 |
| 异步假完成 | 必须手动管理 isExecuting 和 isFinished 的 KVO 通知。 |
| 主从依赖 | 尽量让依赖链中的任务拥有相近的 qualityOfService。 |
💡 进阶建议
当依赖逻辑变得过于复杂时,可以考虑引入 “中间状态检查 Operation” 。在两个业务任务之间插入一个轻量级的 BlockOperation,专门负责检查前序任务的结果并决定是否继续,这样可以有效解耦复