iOS OperationQueue addOperation触发EXC_BAD_ACCESS崩溃
崩溃根因说明
崩溃和OperationQueue.addOperation本身无关,核心是自定义异步Operation的KVO实现不符合系统规范、生命周期被跨线程非法篡改,触发了野指针,具体问题点如下:
- KVO通知逻辑错误,触发内存踩踏
系统OperationQueue完全依赖KVO监听isReady/isExecuting/isFinished三个属性的变化来管理Operation生命周期。你当前实现的statesetter存在两个致命问题:- 状态切换时仅发送了新状态的KVO通知,没有发送旧状态的变更通知。比如从
.ready切到.executing时,只通知了isExecuting变为true,没有通知isReady变为false,队列内部的KVO监听逻辑会拿到错乱的状态值,在后续回调中访问已回收的内存。 - KVO通知的发送和状态修改不在同一个原子执行块内,
willChangeValue/didChangeValue在调用setter的任意线程触发,和内部状态读写队列完全脱节。多线程同时触发取消、重复下载逻辑时,KVO通知会乱序到达,而KVO的观察者列表本身不是线程安全的,乱序触发会直接导致野指针。
这也是为什么Xcode连接调试时无法复现:调试器附着会减慢内存回收速度、打乱线程调度时序,野指针访问刚好命中还未被覆盖的内存,不会触发崩溃;脱离调试器后release模式下内存回收更快,时序刚好触发踩内存。
- 状态切换时仅发送了新状态的KVO通知,没有发送旧状态的变更通知。比如从
- 外部非法篡改Operation生命周期,导致对象提前释放
按照Operation的设计约定,Operation加入队列后,状态流转只能由队列调度、Operation自身逻辑触发,外部仅能调用cancel方法,不能直接修改isFinished状态、提前释放Operation引用。
你当前实现中,cancelOperation和callCompletion方法会跨线程直接调用operation.finish()强制标记完成,还会立刻把Operation从自己的operations字典中删除,此时如果OperationQueue正在遍历内部持有的Operation列表、执行KVO回调,Operation对象已经被提前释放,直接触发EXC_BAD_ACCESS。 - 为什么替换为DispatchQueue后崩溃消失
直接用DispatchQueue.async调用operation.start()完全绕过了OperationQueue的所有KVO监听、生命周期管理逻辑,相当于把Operation当作普通对象执行方法,没有了队列内部的KVO观察者,自然不会因为KVO乱序、提前释放触发野指针。但这种实现完全丢弃了Operation的依赖管理、优先级调度、统一取消等能力,属于临时绕路,没有解决根本问题。
修复方案
- 修正AsyncOperation的KVO和状态管理逻辑
所有状态修改、KVO通知必须放在同一个串行队列的原子块内执行,状态切换时同时通知新旧两个键路径的变化:
同时将private var state: State { get { stateAccessQueue.sync { unsafeState } } set { stateAccessQueue.sync(flags: .barrier) { guard newValue != unsafeState else { return } let oldValue = unsafeState // 严格按照旧值退出、新值进入的顺序发KVO通知 willChangeValue(forKey: oldValue.keyPath) willChangeValue(forKey: newValue.keyPath) unsafeState = newValue didChangeValue(forKey: oldValue.keyPath) didChangeValue(forKey: newValue.keyPath) } } }finish()方法的访问级别收紧,禁止下载器外部直接调用修改状态。 - 将Operation生命周期控制权交还给Operation本身
删除callCompletion中直接调用operation.finish()、提前删除Operation引用的逻辑,在DownloaderOperation内部处理URLSession的下载完成、失败、取消回调,回调触发时自行调用finish()标记完成。
给每个加入队列的Operation设置completionBlock,等Operation真正进入isFinished状态后,再在block中清理字典引用、触发完成回调,保证队列持有Operation期间本地引用不会被提前释放。 - 修复DownloaderOperation的任务赋值时序问题
main方法中先给threadSafeDownloadTask赋值再调用task.resume(),避免resume后立刻触发cancel时拿不到task实例导致取消失效。 - 取消逻辑仅触发cancel,不做额外状态篡改
cancelOperation方法中只调用operation.cancel()即可,不要手动修改download状态为cancelled、不要提前执行完成回调,所有状态变更和回调触发都放到Operation完成后的completionBlock中统一处理。
内容的提问来源于stack exchange,提问作者Shohin
相关产品推荐
相关产品推荐

