关闭含Collection View的视图控制器时触发数组越界崩溃
解决Collection View插入数据时关闭VC导致数组越界崩溃的问题
看起来你遇到的核心问题是后台加载任务没有随着VC的销毁而停止,再加上全局数据源Global.customs的生命周期和VC不一致,导致即使VC被dismiss,后台线程还在修改数据源,进而触发Collection View的插入操作,最终引发数组越界。下面是具体的分析和解决方案:
问题根源拆解
- 后台任务未取消:你启动的后台循环会一直执行完8次迭代,即使VC已经被dismiss。虽然用了
[weak self],但这只会让self?.insertItems()在self为nil时不执行,但后台线程仍在往Global.customs里追加数据,导致数据源和UI状态彻底脱节。 - 全局数据源的风险:
Global.customs是全局变量,生命周期远长于VC,即使你在dismiss前清空它,后台线程后续的append操作仍会修改它,造成数据源和Collection View的预期状态不一致。 - 线程安全缺失:后台线程直接修改全局数组,主线程读取并计算插入的indexPath,存在数据竞争,可能导致计算出的
updatedNumber和实际数据源状态不匹配,进而生成越界的indexPath。
具体解决方案
1. 给后台任务加上取消机制
使用DispatchWorkItem来包装后台任务,这样在VC被销毁时可以直接取消任务,避免后续的无效操作:
// 在VC中定义一个属性来保存后台任务 var loadDataWorkItem: DispatchWorkItem? // 启动加载任务的代码替换为: loadDataWorkItem = DispatchWorkItem { [weak self] in guard let self = self else { return } for customTitle in Global.titles.prefix(8) { // 检查任务是否已被取消,若取消则终止循环 guard !self.loadDataWorkItem!.isCancelled else { break } autoreleasepool { let data = self.getData(name: customTitle) // 切换到主线程更新数据源和UI(确保线程安全) DispatchQueue.main.async { [weak self] in guard let self = self else { return } Global.customs.append(data) self.insertItems() } } } } // 执行后台任务 DispatchQueue.global(qos: .background).async(execute: loadDataWorkItem!)
然后在VC即将销毁时取消任务:
override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if isBeingDismissed { // 取消后台任务 loadDataWorkItem?.cancel() // 清空数据源并同步UI Global.customs.removeAll() collectionView.reloadData() } }
2. 替换全局数据源为VC实例变量
全局变量很容易引发生命周期不匹配的问题,建议把数据源移到VC内部:
// 在VC中定义自己的数据源 private var customData: [YourDataType] = [] // 修改insertItems函数,使用实例变量 func insertItems() { guard let collectionView = self.collectionView, collectionView.window != nil else { // 视图已不在窗口上,无需更新 return } let currentItemCount = collectionView.numberOfItems(inSection: 0) let newItemCount = customData.count let insertCount = newItemCount - currentItemCount guard insertCount > 0 else { return } // 生成安全的indexPath let indexPaths = (0..<insertCount).map { IndexPath(item: currentItemCount + $0, section: 0) } // 额外检查:确保indexPath不超过数据源范围 guard indexPaths.last?.item ?? 0 < newItemCount else { collectionView.reloadData() return } collectionView.insertItems(at: indexPaths) }
这样当VC被销毁时,customData也会被释放,后台任务即使没取消,[weak self]也会阻止无效的UI操作,彻底避免数据源和UI的脱节。
3. 强化insertItems的安全性
在执行插入操作前,先确认Collection View仍处于活跃状态,并且数据源和计算的indexPath完全匹配:
func insertItems() { // 检查Collection View是否还在窗口中(未被销毁) guard let collectionView = self.collectionView, collectionView.isHidden == false, collectionView.window != nil else { return } let currentItemCount = collectionView.numberOfItems(inSection: 0) let newItemCount = customData.count // 或Global.customs.count,建议用实例变量 let insertCount = newItemCount - currentItemCount // 数据不一致时直接reload,避免插入错误 if insertCount <= 0 || currentItemCount + insertCount > newItemCount { collectionView.reloadData() return } let indexPaths = (0..<insertCount).map { IndexPath(item: currentItemCount + $0, section: 0) } collectionView.insertItems(at: indexPaths) }
总结
核心思路是让后台任务的生命周期和VC绑定,同时避免全局数据源带来的状态不一致问题。通过取消后台任务、使用实例变量作为数据源、强化UI操作的安全性,就能彻底解决关闭VC时的数组越界崩溃问题。
内容的提问来源于stack exchange,提问作者Mariela
相关产品推荐
相关产品推荐

