iOS Core Data 并发与批量写入
iOS Core Data 并发与批量写入
Core Data 管理的是对象图,而不仅是数据库表。每个托管对象都属于特定上下文,上下文又绑定自己的执行队列。很多崩溃和数据错乱都来自把托管对象跨队列传递、在主上下文处理大批量数据,或者保存后没有正确合并变化。
一、理解上下文的队列约束
主队列上下文适合界面读取和少量编辑,后台上下文适合导入、清理和批量计算。访问上下文及其对象时,应在该上下文的执行范围内进行。
下面用一个很薄的异步适配器桥接项目已有的后台上下文回调:
private extension NSPersistentContainer {
func performBackground(
_ operation: @escaping (NSManagedObjectContext) throws -> Void
) async throws {
try await withCheckedThrowingContinuation { continuation in
performBackgroundTask { context in
do {
try operation(context)
continuation.resume(returning: ())
} catch {
continuation.resume(throwing: error)
}
}
}
}
}
func importRecords(_ records: [RemoteRecord]) async throws {
try await container.performBackground { context in
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
for record in records {
let item = StoredRecord(context: context)
item.identifier = record.identifier
item.title = record.title
item.updateTime = record.updateTime
}
if context.hasChanges {
try context.save()
}
}
}
具体异步封装应沿用项目现有实现,并确认使用的 API 符合最低系统版本。核心原则是不从外部队列直接操作这个上下文。
二、跨队列传递对象标识
NSManagedObject 不能作为普通模型在线程之间自由传递。跨上下文协作时,应传递 NSManagedObjectID 或稳定业务标识,再在目标上下文中重新获取。
func markAsRead(objectID: NSManagedObjectID) async throws {
try await container.performBackground { context in
guard let record = try context.existingObject(
with: objectID
) as? StoredRecord else {
return
}
record.isRead = true
try context.save()
}
}
新插入但尚未保存的对象可能只有临时标识。如果必须提前传递,需要先获取永久标识,或直接传递业务主键。
三、使用唯一约束实现去重
同步接口数据时,不能每次都盲目插入新对象。可以为稳定业务标识建立唯一约束,并结合合适的合并策略处理冲突。
合并策略不是越强越好。对象优先会保留当前写入值,存储优先会保留已存在值。应逐字段确认服务端数据和本地编辑谁拥有最终解释权。例如“已读”可能是本地优先,而标题通常由服务端更新。
复杂所有权场景可以把本地字段与同步字段拆成不同实体,避免一次合并策略决定所有字段。
四、批量导入要控制内存
一次创建数万托管对象会让上下文保留大量变更和注册对象。可以按批次写入,并在保存后重置后台上下文。
func importInBatches(_ records: [RemoteRecord]) async throws {
let batchSize = 500
try await container.performBackground { context in
for start in stride(from: 0, to: records.count, by: batchSize) {
let end = min(start + batchSize, records.count)
for record in records[start..<end] {
upsert(record, in: context)
}
if context.hasChanges {
try context.save()
context.reset()
}
}
}
}
批次大小要根据对象复杂度和设备表现测量。过小会增加保存次数,过大则提高内存峰值。
五、选择普通写入还是批处理请求
普通托管对象写入会执行对象生命周期和验证逻辑,适合需要完整业务处理的数据。批量插入、更新和删除可以绕过大量对象创建,适合数据量很大且规则简单的场景。
批处理直接作用于持久化存储后,内存中的上下文不会自动获得所有变化。需要根据返回的对象标识合并,或按项目规则刷新相关上下文。否则界面可能继续显示已被删除或更新的旧对象。
使用批处理能力前还要确认最低系统版本,不能直接替换成熟兼容实现。
六、避免在主线程执行重查询
主上下文查询会阻塞界面。查询设计应关注:
- 使用谓词缩小数据范围。
- 只加载需要的属性。
- 为排序和过滤常用字段建立索引。
- 长列表采用分批获取或结果控制器。
- 避免在循环中触发大量关系故障加载。
预取关系能够减少重复访问,但也可能一次加载过多对象。只有确认存在连续关系查询问题时才应启用。
七、管理主上下文合并
后台保存后,主上下文需要感知变化。可以使用容器提供的自动合并配置,或在项目统一持久化组件中手动合并通知。
自动合并也要配合明确的冲突策略。如果用户正在主线程编辑同一个对象,后台同步不应无条件覆盖未提交内容。编辑页可以使用独立子上下文或草稿模型隔离临时修改。
八、把事务边界放在业务操作上
“保存订单并更新账户摘要”如果必须共同成功,就应在同一后台上下文中完成并只保存一次。把它们拆成两个互不相关的保存,会留下中间状态。
网络请求不应放在持久化事务内部。先获得并校验响应,再在短时间上下文操作中完成数据库变更。
九、迁移与数据保护
模型版本变化时,要验证轻量迁移是否真正适用。字段改名、关系重构和数据转换可能需要显式映射或分阶段迁移。
正式环境不能把删除存储并重建当成普通兜底。迁移测试应从真实历史模型生成旧数据,连续升级到当前版本,再验证数量、关系和关键字段。
十、测试并发路径
建议覆盖:
- 后台导入时主列表是否保持流畅。
- 同一业务标识重复导入是否产生重复对象。
- 本地编辑与后台同步冲突时结果是否符合规则。
- 批量删除后主上下文是否及时更新。
- 保存失败时是否留下部分业务状态。
- 大数据导入后的内存峰值是否可接受。
总结
Core Data 并发的核心是上下文隔离。对象只在所属上下文中使用,跨队列传递标识,批量写入控制上下文规模,并把后台变化正确合并到主界面。再结合明确的字段所有权和迁移测试,才能保证数据在高并发与长期升级中保持一致。
- 点赞
- 收藏
- 关注作者
评论(0)