能否安全将NSManagedObject传入performBackgroundTask?需重取对象吗?
首先明确:NSManagedObject实例绝对不是线程安全的,你现在的代码虽然暂时运行正常,但这完全是运气——跨线程访问属于其他上下文的NSManagedObject,随时可能触发崩溃、数据损坏或者不可预测的行为。
performBackgroundTask的作用是帮你创建并管理一个后台专用的NSManagedObjectContext,但它不会自动把主线程的对象“迁移”到后台上下文。你必须在后台上下文里重新获取这些数据,才能安全地执行计算。
为什么现在没崩溃?
可能是因为当前计算只是读取对象属性,没有修改,而且线程调度暂时没出现冲突。但Core Data的线程安全规则是严格的:一个NSManagedObject只能在创建它的上下文所属的线程里访问,哪怕只是读取,跨线程访问都是未定义行为,崩溃只是时间问题。
最佳重取方法:通过ObjectID获取
每个NSManagedObject都有一个线程安全的objectID属性,你可以先在主线程收集所有reports的objectID,然后在后台上下文里用这些ID重新获取对应的对象。这是效率最高、最安全的方式。
完整示例代码
// 第一步:在主线程收集所有reports的objectID(这一步是安全的,因为objectID线程安全) let reportObjectIDs = reports.map { $0.objectID } let persistentContainer = DatabaseStack.shared.persistentContainer persistentContainer.performBackgroundTask { (backgroundContext) in // 第二步:在后台上下文里重新获取对象 var backgroundReports: [Report] = [] for objectID in reportObjectIDs { // 用objectID获取后台上下文对应的对象 if let report = backgroundContext.object(with: objectID) as? Report { backgroundReports.append(report) } } // 第三步:安全执行耗时计算 self.performHeavyCalculations(on: backgroundReports) // 如果计算过程中修改了对象,记得保存后台上下文 do { try backgroundContext.save() // iOS10+默认情况下,主线程上下文会自动合并后台的修改 // 不需要额外操作,主线程UI会自动更新 } catch { print("保存后台上下文失败:\(error)") } } // 你的耗时计算函数示例 func performHeavyCalculations(on reports: [Report]) { // 这里做各种复杂计算,比如统计、求和、排序等 for report in reports { // 安全访问report的属性,因为它属于当前后台上下文 let value = report.someNumericProperty // ... 执行计算逻辑 } }
iOS10环境的注意事项
performBackgroundTask在iOS10是完全可用的,它会自动处理后台上下文的创建、关联到持久化存储协调器,以及上下文的销毁。你只需要遵守:
- 所有在后台上下文里的操作都要在
performBackgroundTask的闭包内完成 - 不要把后台上下文的对象带出闭包(或者跨线程访问)
- 如果需要保存修改,必须在闭包内调用
backgroundContext.save()
另一种可选方法:用Fetch Request重取
如果你的reports是通过某个条件查询出来的,也可以在后台上下文里执行相同的Fetch Request来获取数据,比如:
persistentContainer.performBackgroundTask { (backgroundContext) in let fetchRequest: NSFetchRequest<Report> = Report.fetchRequest() // 设置和主线程相同的查询条件,比如predicate、sortDescriptors等 fetchRequest.predicate = NSPredicate(format: "category == %@", "某分类") do { let backgroundReports = try backgroundContext.fetch(fetchRequest) self.performHeavyCalculations(on: backgroundReports) } catch { print("重取数据失败:\(error)") } }
这种方法适合你不持有原reports数组,而是知道查询条件的场景。但如果已经有了原数组,用ObjectID重取更高效。
总结:一定要重新获取,不要依赖当前的“正常运行”,否则上线后大概率会出现随机崩溃,很难排查。
内容的提问来源于stack exchange,提问作者Kamil Harasimowicz

