CoreData与NSFetchedResultsController并发增删时崩溃求助
我之前也踩过这个棘手的偶发崩溃坑!从你给出的崩溃日志和代码来看,问题出在NSFetchedResultsController的代理回调与UITableView更新的时机不匹配——当同时处理插入、删除这类变更时,逐次调用insertRows/deleteRows很容易导致表格的索引计算混乱,最终触发断言失败。
核心原因分析
崩溃日志里的关键信息已经点明了问题本质:
Invalid update: invalid number of rows in section 7. The number of rows contained in an existing section after the update (4) must be equal to the number of rows contained in that section before the update (4), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted)
这说明表格在计算更新前后的行数量时,和实际执行的插入/删除操作对不上。根源是你当前的代码在didChange回调里立即操作表格,当同一section同时有插入和删除时,插入操作会改变后续行的索引,后续的删除操作就会用错误的索引去执行,导致最终的行数量校验失败。
亲测有效的解决方案:批量收集变更后统一更新
不要在didChange里直接操作表格,而是先把所有变更事件收集起来,最后在controllerDidChangeContent里一次性批量执行更新,同时注意先删后插的顺序,避免索引偏移。
步骤1:定义变更存储数组
在你的ViewController里添加几个数组,用来暂存各类变更:
private var insertedIndexPaths: [IndexPath] = [] private var deletedIndexPaths: [IndexPath] = [] private var updatedIndexPaths: [IndexPath] = [] private var movedIndexPaths: [(from: IndexPath, to: IndexPath)] = []
步骤2:在变更开始前清空数组
实现controllerWillChangeContent代理方法,每次开始变更前重置存储数组:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { insertedIndexPaths.removeAll() deletedIndexPaths.removeAll() updatedIndexPaths.removeAll() movedIndexPaths.removeAll() }
步骤3:收集所有变更事件
修改原来的didChange方法,只收集变更信息,不直接操作表格:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case .insert: guard let newIndexPath = newIndexPath else { return } insertedIndexPaths.append(newIndexPath) case .delete: guard let indexPath = indexPath else { return } deletedIndexPaths.append(indexPath) case .update: guard let indexPath = indexPath else { return } updatedIndexPaths.append(indexPath) case .move: guard let fromIndexPath = indexPath, let toIndexPath = newIndexPath, fromIndexPath != toIndexPath else { return } movedIndexPaths.append((from: fromIndexPath, to: toIndexPath)) @unknown default: break } }
步骤4:批量执行表格更新
修改controllerDidChangeContent方法,用performBatchUpdates统一处理所有变更,注意先处理删除再处理插入:
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.performBatchUpdates({ // 先删除,避免插入导致索引偏移 if !deletedIndexPaths.isEmpty { tableView.deleteRows(at: deletedIndexPaths, with: .none) } // 再插入 if !insertedIndexPaths.isEmpty { tableView.insertRows(at: insertedIndexPaths, with: rowAnimation) } // 更新行 updatedIndexPaths.forEach { self.updateRow(atIndexpath: $0) } // 处理移动 movedIndexPaths.forEach { move in tableView.moveRow(at: move.from, to: move.to) self.updateRow(atIndexpath: move.to) } }, completion: nil) }
额外注意事项
- 线程安全:确保所有Core Data的插入/删除操作都在主线程上下文执行(或者通过
performBlock回到主线程),避免并发操作导致的数据状态不一致。 - 动画排查:如果还是偶发崩溃,可以暂时把动画改成
.none,排除动画本身导致的时序问题。 - FetchRequest校验:检查你的FetchedResultsController的
fetchRequest,确保排序、过滤条件正确,避免变更后的数据和表格预期的行数量不匹配。
内容的提问来源于stack exchange,提问作者Sathish Kumar Gurunathan

