使用场景
在使用Core Data的过程中,我需要iOS应用在打开后,从CSV文件中将历史的汇率数据全部解析并存储到Core Data中。

从2025年到1999年,共41中货币的汇率数据,计算起来大概27万多条数据。
在使用Core Data插入数据时,使用Date计算插入时间为 143 秒,将近2分半钟,用时太长。

因此,考虑使用NSBatchInsertRequest方法来插入这些历史汇率数据。
NSBatchInsertRequest
NSBatchInsertRequest 是从 iOS 13 / macOS 10.15 开始引入的 Core Data 新 API,用于高效地批量插入大量数据,而不会将所有 NSManagedObject 加载到内存中。
优点:
极大地减少内存消耗
插入速度快(可比普通插入快几十倍)
插入时不会触发 KVO、不会加入上下文中
不适合:
需要操作托管对象(如设置关系、访问属性)时
插入后立即在界面上展示数据(因为不会自动更新 context)
使用方法
1、使用 Dictionary 数组(推荐)
可以用 [Dictionary<String, Any>] 数据表示要插入的对象集合。
let itemsToInsert: [[String: Any]] = [
["name": "USD", "rate": 1.0],
["name": "EUR", "rate": 0.92],
// 更多字典...
]
let insertRequest = NSBatchInsertRequest(entityName: "Currency", objects: itemsToInsert)
do {
try context.execute(insertRequest)
} catch {
print("批量插入失败: \(error)")
}
这里的itemsToInsert是一个字典类型的数组,包含了需要插入的大量数据。
NSBatchInsertRequest的entityName是Core Data 模型中定义的实体名称,objects是插入的对象集合。
最后,使用context将数据插入到Core Data中,这里推荐使用backgroundContext处理大量数据,以免影响到UI视图。
此外,NSBatchInsertRequest还有另一种使用entity初始化方法:
let batchInsertRequest = NSBatchInsertRequest(entity: Eurofxrefhist.entity(), objects: records)
entity是对应的Core Data实体,通常通过 YourEntity.entity() 获取。
区别在于前者只需要传入字符串,后者传入实体描述对象。
提示:使用 YourEntity.entity() 可以避免拼写错误,尤其适合有代码提示的环境。
2、使用闭包(Block-Based)
可以逐个创建实体对象,系统会在内部批量处理,提高性能。
let insertRequest = NSBatchInsertRequest(entity: Currency.entity()) { (managedObject: NSManagedObject) -> Bool in
guard let currency = managedObject as? Currency else { return true }
// 设置属性
currency.name = "USD"
currency.rate = 1.0
// 返回 false 继续插入,true 停止插入
return false
}
do {
try context.execute(insertRequest)
} catch {
print("批量插入失败: \(error)")
}
注意事项
NSBatchInsertRequest 会绕过上下文的管理,所以插入的数据不会自动反映到当前 NSManagedObjectContext 中,除非手动刷新或重新 fetch。
如果需要在 UI 中立即看到变化,请调用:
context.refreshAllObjects() // 或使用 merge 政策
这个方法会让当前 NSManagedObjectContext 的所有托管对象都重新加载(从数据库重新拉数据),相当于把 context 里的缓存清空了,强制它下次 fetch 时重新读数据库。
或者设置返回 NSManagedObjectID 的选项:
insertRequest.resultType = .objectIDs
let result = try context.execute(insertRequest) as? NSBatchInsertResult
let insertedIDs = result?.result as? [NSManagedObjectID]
let changes = [NSInsertedObjectsKey: insertedIDs ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
resultType = .objectIDs:告诉 Core Data,插入后返回新插入对象的 NSManagedObjectID 列表。
用 mergeChanges(fromRemoteContextSave:into:) 手动将这些 ID 的变化合并进当前 context。
只刷新插入的对象,性能好,推荐在UI项目中使用。
这样可以让插入的数据自动合并到当前 context 中。
实际应用
返回到最开始的使用场景,下面是拆分的逻辑代码:
func processCSVData(_ filePath: String) {
// 读取并解析CSV文件
let startDate = Date()
print("开始解析CSV文件")
do {
// 省略拆分代码逻辑
// 处理每一行
for line in lines {
// 省略日期代码
if let date = dateFormatter.date(from: dateString) {
// 处理汇率数据
var exchangeRates: [String: Double] = [:]
// 移除每一行的第一个日期字段后,使用 enumerated() 设置序号
for (currencyCode, column) in zip(CurrencyCodes, columns.dropFirst()) {
let rate = Double(column) ?? 0.0
exchangeRates[currencyCode] = rate
}
// 保存到Core Data
saveExchangeRates(date: date, rates: exchangeRates)
} else {
print("处理汇率数据时,日期解码失败")
}
}
let endDate = Date()
let interval = endDate.timeIntervalSince(startDate)
print("所有汇率数据处理完成,用时:\(interval)秒")
} catch {
print("读取CSV失败: \(error)")
}
}
我拆分CSV文件后,会获得6700多行的汇率数据。
通过for-in函数遍历每一行的41中货币的汇率数据,将对应日期的数据存储在exchangeRates字典中,调用saveExchangeRates方法,保存汇率数据。
saveExchangeRates方法代码:
func saveExchangeRates(date: Date, rates: [String: Double]) {
fetchRequest.predicate = NSPredicate(format: "date == %@", date as CVarArg)
do {
// 获取过滤的数据
let existingRecords = try context.fetch(fetchRequest)
// 如果没有当前的日期,则插入新的数据
if existingRecords.isEmpty {
// 如果没有找到该日期的数据,插入新的记录
for (currency, rate) in rates {
let exchangeRate = Eurofxrefhist(context: context)
exchangeRate.date = date
exchangeRate.currencySymbol = currency
exchangeRate.exchangeRate = rate
}
} else {
print("该日期的数据已存在,跳过:\(date)")
}
// 一次性保存所有修改
try context.save()
print("所有数据保存成功")
} catch {
print("保存数据失败: \(error)")
}
}
使用NSFetchRequest筛选Core Data中同一日期的汇率数据。
这里假设汇率数据是2025年的4月11日,如果Core Data中没有这一天的汇率数据,那么就把4月11日的汇率数据插入到Core Data中,全部插入后再一次性保存所有修改。
这样,每次都会把对应日期的汇率数据,经过一系列判定后,插入到Core Data中。

但是,目前的问题就在于使用这一方式需要2分钟的时间才能把Core Data数据全部同步一遍,在实际应用中时间因素不合理,就需要进一步优化。
使用NSBatchInsertRequest
func processCSVData(_ filePath: String) {
let startDate = Date()
// 读取并解析CSV文件
do {
// 省略拆分代码逻辑
// 构造用于批量插入的数据数组
var records: [[String: Any]] = []
// 处理每一行
for line in lines {
// 省略日期代码
if let date = dateFormatter.date(from: dateString) {
// 移除每一行的第一个日期字段后,使用 enumerated() 设置序号
for (currencyCode, column) in zip(CurrencyCodes, columns.dropFirst()) {
let rate = Double(column) ?? 0.0
let record: [String: Any] = [
"date": date,
"currencySymbol": currencyCode,
"exchangeRate": rate
]
records.append(record)
}
} else {
print("处理汇率数据时,日期解码失败")
}
}
print("CSV 解析完成,共解析出 \(records.count) 条记录")
// 直接使用 NSBatchInsertRequest 批量插入 Core Data
batchInsertExchangeRates(records: records)
print("所有汇率数据处理完成,用时:\(interval)秒")
} catch {
print("读取CSV失败: \(error)")
}
}
在这里的改动是,不再将每一天的汇率调用Core Data存储,而是创建了一个用于批量插入的数据数组records。
var records: [[String: Any]] = []
使用for-in方法遍历每一天的历史汇率数据,将每一天的41个货币的汇率数据插入到records数组中。
最后调用batchInsertExchangeRates方法,将records数组传入到该方法中。
func batchInsertExchangeRates(records: [[String: Any]]) {
// 使用后台上下文进行插入,确保 UI 不会卡顿
let backgroundContext = container.newBackgroundContext()
backgroundContext.perform {
// 构造批量插入请求,实体名称对应你的 Core Data 模型中定义的实体
let batchInsertRequest = NSBatchInsertRequest(entityName: "Eurofxrefhist", objects: records)
do {
try backgroundContext.execute(batchInsertRequest)
print("批量插入成功,共 \(records.count) 条记录")
} catch {
print("批量插入失败:\(error)")
}
}
}
在batchInsertExchangeRates方法中,使用后台上下文backgroundContext构造NSBatchInsertRequest。
backgroundContext的perform异步执行,不会阻塞当前进程。
使用构造方法创建NSBatchInsertRequest,使用backgroundContext将数据插入到Core Data中,完成全部的优化工作。

通过使用NSBatchInsertRequest将27万条数据使用了11秒的时间,全部插入到Core Data,预计节省了2分钟的时间。

总结
在处理耗时任务时,应该考虑优化耗时时间,比如通过使用NSBatchInsertRequest来处理插入大量数据的工作,以及使用Backgroundcontext后台任务,避免影响UI前台的操作。
扩展知识
NSBatchInsertRequest数据的写入是否会中断?
假设需要在Core Data中,插入2025年到1999年的全部汇率数据。如果在插入的过程中,iOS应用关闭,重新打开时是否会出现残留的数据,整个数据插入流程是全部插入后保存,还是批量插入保存?
NSBatchInsertRequest 是 Core Data 专为高性能批量写入设计的。它的行为取决于初始化方式,但对于当前这种写法:
let batchInsertRequest = NSBatchInsertRequest(entityName: "Eurofxrefhist", objects: records)
try backgroundContext.execute(batchInsertRequest)
它是将整批数据一次性插入到底层 SQLite 中(绕过上下文管理),而不是一条一条插入和保存。
它不会触发每条记录的 NSManagedObject lifecycle(如 willSave/didSave)。
它直接写入持久化存储,不依赖手动调用 backgroundContext.save()。
如果中途失败,情况分为两种:
1、成功前被中断:不保存任何记录:NSBatchInsertRequest 是原子的。也就是说如果传入的是一个对象数组,它要么全部成功,要么全部失败。
2、插入逻辑里包含了不合法字段:某一条插入数据格式不符合实体定义(比如 nil 传给 non-optional 字段),那整批失败。所以在构建 records: [[String: Any]] 时,必须保证数据格式安全(每个字段的值必须符合在 Core Data 实体中设定的属性类型、非空规则、约束条件。)。
相关文章
1、Core Data NSManagedObjectContext的后台上下文backgroundContext:https://fangjunyu.com/2025/03/31/core-data-nsmanagedobjectcontext%e7%9a%84%e5%90%8e%e5%8f%b0%e4%b8%8a%e4%b8%8b%e6%96%87backgroundcontext/
2、Core Data获取数据的NSFetchRequest:https://fangjunyu.com/2025/04/10/core-data%e8%8e%b7%e5%8f%96%e6%95%b0%e6%8d%ae%e7%9a%84nsfetchrequest/