CoreData多上下文操作报跨存储引用/集合枚举错误排查
问题根因
所有异常和数据缺失问题全部来自CoreData并发规则违反和逻辑漏洞,具体对应:
Cannot save objects with references outside their own stores:NSManagedObject实例和创建它的上下文强绑定,禁止跨上下文直接传对象引用做关联。代码中managedFoo是后台上下文创建的,却直接被加到主线程上下文查询出的bar的关联集合里,两个对象分属不同上下文管辖,保存时直接触发跨引用错误。Collection <__NSCFSet: ...> was mutated while being enumerated:NSManagedObjectContext不是线程安全的,主线程上下文(viewContext)的所有操作必须在主线程执行。代码中bar是从主线程上下文查出来的,但bar.addToData(managedFoo)的执行线程没有任何约束,如果saveFoo在后台线程调用,就会在后台线程修改主线程持有的有序集合,刚好此时主线程在做集合枚举、变更合并操作,就会触发并发修改崩溃。- 偶发数据缺失是四个逻辑问题叠加导致:
- 当查询到已存在的
bar时,只修改了bar的关联集合,既没有保存主线程上下文的改动,也没有把改动同步到后台上下文,关联关系根本没有持久化 - 原
saveContext方法用backgroundContext.perform做异步派发,调用后立刻返回,没有等待保存完成就执行后续查询,很容易读到保存前的旧数据 - 新建
bar的分支里,根本没有把提前创建的managedFoo加到bar的data集合里,foo和bar的关联完全丢失 getAllBarNames方法里写错了查询实体,用的是ManagedEvent.fetchRequest()而不是ManagedBar,本身就查不到目标数据
- 当查询到已存在的
修复方案
核心遵守CoreData并发铁则:任何NSManagedObject的增删改查操作,必须在它所属上下文的perform/performAndWait块内执行,绝对不能跨上下文直接传递对象引用,跨上下文传值只能用NSManagedObjectID。
1. 修正CoreDataStack基础逻辑
去掉阻塞加载、异步保存无等待的问题,统一上下文操作入口:
class CoreDataStack { // 用单例保证持久化栈唯一,避免多实例导致的存储冲突 static let shared = CoreDataStack() static let modelName = "Model" static let managedObject: NSManagedObjectModel = { guard let modelUrl = Bundle.module.url( forResource: "Model", withExtension: "momd" ) else { // 模型加载失败直接崩溃,早暴露问题不要返回空模型 fatalError("CoreData模型文件不存在,检查target资源配置") } guard let model = NSManagedObjectModel(contentsOf: modelUrl) else { fatalError("CoreData模型初始化失败") } return model }() lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer( name: Self.modelName, managedObjectModel: Self.managedObject ) container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("持久化存储加载失败 \(error), \(error.userInfo)") } }) return container }() var mainContext: NSManagedObjectContext { let ctx = persistentContainer.viewContext ctx.automaticallyMergesChangesFromParent = true ctx.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump return ctx } // 统一后台写操作入口,块执行完自动保存 func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) { persistentContainer.performBackgroundTask { bgContext in bgContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump block(bgContext) if bgContext.hasChanges { do { try bgContext.save() } catch { print("后台上下文保存失败: \(error)") } } } } // 统一查询入口,保证所有context操作在对应队列执行 func fetch<Item: NSManagedObject>( _ request: NSFetchRequest<Item>, context: NSManagedObjectContext? = nil ) -> [Item] { let targetContext = context ?? mainContext var results: [Item] = [] targetContext.performAndWait { do { results = try targetContext.fetch(request) } catch { print("查询失败: \(error)") results = [] } } return results } }
2. 修正saveFoo方法
所有写操作全放在后台上下文执行,全程不跨context操作对象:
func saveFoo(_ foo: Foo) { coreDataStack.performBackgroundTask { bgContext in // 直接在后台上下文查询关联的Bar,不要去主线程查了跨线程传 let barRequest = ManagedBar.fetchRequest() barRequest.predicate = NSPredicate(format: "name == %@", foo.name) let existBars = try? bgContext.fetch(barRequest) let targetBar: ManagedBar if let existBar = existBars?.first { targetBar = existBar } else { // 不存在则在当前后台上下文新建Bar targetBar = ManagedBar(context: bgContext, name: foo.name) } // 同一个上下文下创建Foo、建立关联,不存在跨引用问题 let managedFoo = ManagedFoo(context: bgContext, name: foo.name) targetBar.addToData(managedFoo) // 块执行结束自动触发保存,不用手动调用save } }
3. 修正getAllBarNames方法
修正实体类错误,保证主线程上下文查询只在主线程执行:
func getAllBarNames() -> [String] { // 修正之前写错的实体类,从ManagedEvent改成ManagedBar let fetchRequest = ManagedBar.fetchRequest() var names: [String] = [] if Thread.isMainThread { let barList = coreDataStack.fetch(fetchRequest) names = barList.map { $0.name } } else { DispatchQueue.main.sync { let barList = coreDataStack.fetch(fetchRequest) names = barList.map { $0.name } } } return names }
4. 测试逻辑适配异步保存
因为后台保存是异步执行,测试时需要等待操作完成再校验结果:
func testGetAllEventNames() { let saveExpect = expectation(description: "保存数据完成") let foo1 = Foo(name: "test1") saveFoo(foo1) let foo2 = Foo(name: "test2") saveFoo(foo2) // 等待后台保存完成、主线程上下文合并完变更再查询 DispatchQueue.main.asyncAfter(deadline: .now() + 1) { let names = getAllBarNames() print(names) // 这里加对应断言校验数据正确性 saveExpect.fulfill() } wait(for: [saveExpect], timeout: 3) }
内容的提问来源于stack exchange,提问作者Tometoyou
相关产品推荐
相关产品推荐

