Highlight
Apple 演示了 Core Data 的 batch insert block 变体、batch update/delete、fetch 裁剪和 ObjectID 通知,帮助开发者降低大批量数据导入、查询和持久化变更响应的内存与时间成本。
核心内容
很多 Core Data App 都有同一个开场:网络拿到一大批 JSON,后台 context 把它们变成 managed object,保存到本地 store,再让 view context 合并变化来更新 UI。Earthquakes 示例就是这样工作,它从 USGS feed 读取地震数据。数据量一大,后台 context 会创建或 fetch 出很多对象,这些对象保存后很快就被丢掉。(00:32)
这场 session 的主线是把这类重活搬到更轻的 Core Data 路径上。批量操作直接处理 insert、update、delete,可以减少 managed object 的创建成本。它们不会发出普通 save notification,也不会触发 accessor 或 callback,所以 Apple 把 persistent history 放在同一条链路里:先记录批量操作,再用历史记录驱动通知和局部刷新。(01:16)
后半段讨论 fetch 和通知。列表页只显示十几行数据时,没有必要把全部对象都 hydrate 到内存;跨线程处理时,也不适合直接传 managed object。Core Data 提供 batch size、propertiesToFetch、relationship prefetch、ObjectID result、dictionary result 和 count result。store 被扩展、另一个 App 或 share extension 修改时,ObjectID 通知、remote change notification 和 persistent history 可以让 App 知道具体变化来自哪里。(08:25、13:06)
详细内容
开启 persistent history 承接批量操作(01:43)
批量操作的代价很明确:它们没有普通保存流程,所以不会产生 save notification;它们也没有实例化 managed object,所以 accessor 和 callback 不会被调用。Session 给出的补救方式是开启 persistent history,让 batch operation 被记录下来,后续再从历史记录里找到当前视图关心的变化。
storeDesc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
关键点:
storeDesc是 persistent store description。NSPersistentHistoryTrackingKey开启后,batch insert/update/delete 会进入 persistent history。- 这段配置解决的是“怎么知道 store 发生了什么”,不负责自动执行 managed object 的 accessor 逻辑。
- 如果 UI 只关心某些对象,需要再按 ObjectID 或时间范围筛选 persistent history。
用 NSBatchInsertRequest 降低导入峰值(02:15)
NSBatchInsertRequest 最初支持传入字典数组。iOS 14 增加了 handler 形式,可以让 Core Data 反复向你的 block 要下一条字典或 managed object。Transcript 明确说,这能降低导入过程的峰值内存,也能减少对象分配。
//NSBatchInsertRequest.h
@available(iOS 13.0, *)
open class NSBatchInsertRequest : NSPersistentStoreRequest {
open var resultType: NSBatchInsertRequestResultType
public convenience init(entityName: String, objects dictionaries: [[String : Any]])
public convenience init(entity: NSEntityDescription, objects dictionaries: [[String : Any]])
@available(iOS 14.0, *)
open var dictionaryHandler: ((inout Dictionary<String, Any>) -> Void)?
open var managedObjectHandler: ((inout NSManagedObject) -> Void)?
public convenience init(entity: NSEntityDescription, dictionaryHandler handler: @escaping (inout Dictionary<String, Any>) -> Void)
public convenience init(entity: NSEntityDescription, managedObjectHandler handler: @escaping (inout NSManagedObject) -> Void)
}
关键点:
- 字典数组 initializer 适合已经把输入整理成
[[String: Any]]的场景。 dictionaryHandler和managedObjectHandler是 iOS 14 增加的入口。- handler 让 Core Data 按需取得下一条数据,避免先构造完整数组。
resultType决定执行后返回什么结果,具体值需要按写入后的合并需求选择。
Earthquakes 示例先展示传统保存:为每条 quake 创建 managed object,填值,然后 save()。
//Earthquakes Sample - Regular Save
for quakeData in quakesBatch {
guard let quake = NSEntityDescription.insertNewObject(forEntityName: "Quake", into: taskContext) as? Quake else { ... }
do {
try quake.update(with: quakeData)
} catch QuakeError.missingData {
...
taskContext.delete(quake)
}
...
}
do {
try taskContext.save()
} catch { ... }
关键点:
- 循环里会创建大量
Quakemanaged object。 - 每个对象都要执行属性更新和错误处理。
- 最后一次
save()会带来大量变化合并。 - Session 的测量结果显示,这条路径导入大量地震数据超过一分钟,idle memory 约 30 MB。
同样的数据改成 block 形式的 batch insert:
//Earthquakes Sample - Batch Insert with a block
var batchInsert = NSBatchInsertRequest(entityName: "Quake", dictionaryHandler: {
(dictionary) in
if (blockCount == batchSize) {
return true
} else {
dictionary = quakesBatch[blockCount]
blockCount += 1
}
})
var insertResult : NSBatchInsertResult
do {
insertResult = try taskContext.execute(batchInsert) as! NSBatchInsertResult
...
}
关键点:
dictionaryHandler每次填充一条 quake 字典。- 返回
true表示数据已经填完,Core Data 可以停止请求下一条。 taskContext.execute(batchInsert)直接执行 persistent store request。- Session 的测量结果显示,字典数组 batch insert 用 13 秒完成,block ingestion 用 11 秒完成。
用 unique constraint 和 merge policy 做 UPSERT(04:33)
Earthquakes feed 每次都会返回最近 30 天的数据。大部分 quake 已经存在,只有少数是新增或更新。示例把 quake 的 code 设为 unique constraint,然后在执行 batch insert 的 context 上设置 merge policy。遇到相同 code 时,Core Data 会更新已有行,而不是删除旧对象再插入新对象。
let moc = NSManagedObjectContext(concurrencyType:
NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType)
moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
insertResult = try moc.execute(insertRequest)
关键点:
- unique constraint 放在 Core Data model 里,示例用的是 quake 的
code。 NSMergeByPropertyObjectTrumpMergePolicy让当前对象的属性覆盖 store 中冲突对象的属性。- merge policy 必须设置在执行 batch insert request 的 context 上。
- 这段逻辑对应 transcript 中的 SQL UPSERT:插入时遇到冲突就更新指定属性。
用 batch update/delete 避免拉全量对象(06:10)
如果只是把符合条件的 quake 标记为已验证,先 fetch 出所有对象、逐个改属性、再保存,会把很多不需要展示的对象拉进内存。NSBatchUpdateRequest 可以直接把谓词命中的对象属性改掉。
//Earthquakes Sample - Batch Update
let updateRequest = NSBatchUpdateRequest(entityName: "Quake")
updateRequest.propertiesToUpdate = ["validated" : true]
updateRequest.predicate = NSPredicate("%K > 2.5", "magnitude")
var updateResult : NSBatchUpdateResult
do {
updateResult = try taskContext.execute(updateRequest) as! NSBatchUpdateResult
...
}
关键点:
NSBatchUpdateRequest(entityName:)指向要更新的 entity。propertiesToUpdate写出要改的字段和值。predicate限制更新范围,示例只更新magnitude > 2.5的 quake。execute后返回NSBatchUpdateResult,不会逐个实例化 managed object。
删除也适合走 batch path。示例删除 30 天前的 quake,并且给 batch delete 设置 fetchLimit,避免一次写锁占用 store 太久。
// Batch Delete without and with a Fetch Limit
DispatchQueue.global(qos: .background).async {
moc.performAndWait { () -> Void in
do {
let expirationDate = Date.init().addingTimeInterval(-30*24*3600)
let request = NSFetchRequest<Quake>(entityName: "Quake")
request.predicate = NSPredicate(format:"creationDate < %@", expirationDate)
let batchDelete = NSBatchDeleteRequest(fetchRequest: request)
batchDelete.fetchLimit = 1000
moc.execute(batchDelete)
}
}
}
关键点:
- 清理任务放在 background queue,实际 Core Data 操作仍进入
moc.performAndWait。 creationDate < expirationDate是删除条件。NSBatchDeleteRequest(fetchRequest:)复用 fetch request 定义删除范围。fetchLimit = 1000把一次删除控制在有界范围内,减少长时间写锁。
按视图需求裁剪 fetch(08:25)
Fetch 的默认结果通常是 managed object。它适合完整遍历对象图,也适合让 fetched results controller 跟随对象更新。但列表一次只显示少量行时,可以设置 batch size,让 Core Data 先 hydrate 第一批对象,滚动到后面时再继续加载。Session 的测量里,同样的 Earthquakes 列表从约 17 MB idle memory 降到约 12 MB。
另一个裁剪点是属性和关系。已知 UI 只显示部分字段时,可以设置 propertiesToFetch。已知某条 relationship 很可能被访问时,可以预取对应 key path,避免第一次遍历关系时逐个触发 fault。跨线程传递结果时,ObjectID result 比 managed object 更合适;只读聚合数据可以用 dictionary result。
//Fetch average magnitude of each place
let magnitudeExp = NSExpression(forKeyPath: "magnitude")
let avgExp = NSExpression(forFunction: "avg:", arguments: [magnitudeExp])
let avgDesc = NSExpressionDescription()
avgDesc.expression = avgExp
avgDesc.name = "average magnitude"
avgDesc.expressionResultType = .floatAttributeType
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Quake")
fetch.propertiesToFetch = [avgDesc, "place"]
fetch.propertiesToGroupBy = ["place"]
fetch.resultType = .dictionaryResultType
关键点:
NSExpression(forKeyPath: "magnitude")选中要聚合的属性。NSExpression(forFunction: "avg:", arguments:)定义平均值计算。NSExpressionDescription给聚合结果命名并声明结果类型。propertiesToFetch同时包含聚合字段和分组字段。propertiesToGroupBy = ["place"]按地点分组。dictionaryResultType返回轻量只读结果,适合跨线程传递或直接驱动统计视图。
用 ObjectID 通知和 remote change 响应 store 变化(13:16)
iOS 14 给 NSManagedObjectContext 增加了更适合 Swift 的 notification name,也加入了 ObjectID 版本的通知。ObjectID notification 是 managed object save notification 的轻量对应物,由 persistent history transaction 提供。
//NSManagedObjectContext.h
@available(iOS 14.0, *)
extension NSManagedObjectContext {
public static let willSaveObjectsNotification: Notification.Name
public static let didSaveObjectsNotification: Notification.Name
public static let didChangeObjectsNotification: Notification.Name
public static let didSaveObjectIDsNotification: Notification.Name
public static let didMergeChangesObjectIDsNotification: Notification.Name
}
关键点:
willSaveObjectsNotification、didSaveObjectsNotification、didChangeObjectsNotification是现代化后的 Swift 名称。didSaveObjectIDsNotification和didMergeChangesObjectIDsNotification传递 ObjectID,避免把完整 managed object 放进通知。- 这类通知适合只需要识别“哪些对象变了”的 UI 或后台处理。
- Session 也列出了新的
NotificationKey,包括 inserted、updated、deleted 对象和 ObjectID 版本。
如果同一个 store 被 share extension、第二个 App 或 Photos extension 修改,App 过去往往要在回到前台时 poll persistent history。开启 remote change notification 后,store 被 Core Data client 修改时可以直接通知当前 App。
storeDesc.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
storeDesc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
关键点:
NSPersistentStoreRemoteChangeNotificationPostOptionKey开启 remote change notification。NSPersistentHistoryTrackingKey让通知的userInfo能带上 persistent history token。- token 可以继续用于读取 persistent history,并转成 ObjectID 层面的变化。
- 这条链路覆盖进程内外的 Core Data client 变化,比前台轮询更直接。
persistent history 本身也要裁剪。Session 最后展示了按 ObjectID 查历史变化的请求。
let changeDesc = NSPersistentHistoryChange.entityDescription(with: moc)
let request = NSFetchRequest<NSFetchRequestResult>()
//Set fetch request entity and predicate
request.entity = changeDesc
request.predicate =
NSPredicate(format: "%K = %@",changeDesc?.attributesByName["changedObjectID"], objectID)
//Set up history request with distantPast and set fetch request
let historyReq = NSPersistentHistoryChangeRequest.fetchHistory(after: Date.distantPast)
historyReq.fetchRequest = request
let results = try moc.execute(historyReq)
关键点:
NSPersistentHistoryChange.entityDescription(with:)取得 history change 的 entity description。- fetch request 的 predicate 指向
changedObjectID。 fetchHistory(after:)定义历史查询起点。historyReq.fetchRequest = request把普通 fetch 的裁剪条件应用到 history request。- 执行后只返回指定 ObjectID 在给定时间之后的变化。
核心启发
-
做什么:给新闻、天气、行情类 App 的后台导入改用 batch insert。为什么值得做:Session 的 Earthquakes 示例里,传统保存大量对象超过一分钟,batch insert 把同样导入降到十几秒。怎么开始:先把输入数据整理成 entity 属性字典;如果数据源很大,再改成
NSBatchInsertRequest(entityName:dictionaryHandler:),并为 store 开启 persistent history。 -
做什么:把重复导入的 feed 设计成 UPSERT。为什么值得做:最近 30 天的地震 feed 大多是旧数据,唯一
code加 merge policy 可以更新已有行,避免删除再插入。怎么开始:在 Core Data model 里给业务唯一键设置 unique constraint,在执行 batch insert 的 context 上设置NSMergeByPropertyObjectTrumpMergePolicy。 -
做什么:给定期清理任务加 batch delete 和 fetch limit。为什么值得做:清理过期对象会拿 store 写锁;没有上限时,大量匹配对象可能让用户等待很久。怎么开始:用
NSFetchRequest写出过期条件,创建NSBatchDeleteRequest,按数据规模设置fetchLimit,让任务分批运行。 -
做什么:为大列表做 fetch 裁剪。为什么值得做:Session 展示了 batch size 能把 Earthquakes 列表的 idle memory 从约 17 MB 降到约 12 MB。怎么开始:先确认首屏需要的字段,设置 batch size 和
propertiesToFetch;如果 cell 一定会访问关系,再配置 relationship prefetch。 -
做什么:用 remote change notification 替代前台轮询。为什么值得做:share extension、第二个 App 或扩展修改同一个 store 时,App 可以通过 notification 和 persistent history token 找到具体变化。怎么开始:同时开启
NSPersistentStoreRemoteChangeNotificationPostOptionKey和NSPersistentHistoryTrackingKey,收到通知后按 ObjectID 或时间范围查询 history。
关联 Session
- Sync a Core Data store with the CloudKit public database — 继续学习 Core Data store 与 CloudKit public database 的同步路径。
- Data Essentials in SwiftUI — 理解 SwiftUI 如何围绕 source of truth 和 data model 更新视图。
- Advances in diffable data sources — 把 Core Data 变化转换成 collection view 和 table view 的稳定差异更新。
- Advances in UICollectionView — 学习现代 collection view 列表、outline 和 cell 配置,承接 Core Data fetch 结果展示。
- Diagnose performance issues with the Xcode Organizer — 用线上聚合指标追踪磁盘写入、滚动卡顿和性能回归。
评论
GitHub Issues · utterances