Ed92d9753ba62705435374f5e0096aae
WWDC20 10017 - Core Data 杂项与准则

WWDC20 10017 - Core Data 杂项与准则

本文是对 WWDC 2020 Core Data: Sundries and maxims Session 的翻译。

本 Session 将从三个方面介绍如何优化 Core Data 性能:

  • 使用批量操作
  • 定制查询操作
  • 响应变化通知

下面会以这个地震信息列表 Demo 为例,说明上面的内容:


示例中,从 USGS(美国地质调查局) 获取到 JSON Feed 通过 JSON Parser 解析后,通过 Background Context 存入到持久化存储中,View Context 合并过数据后交给显示层。

在上面的过程中,大量的 Managed Object 在被创建保存之后立即就被废弃了。这正是批量操作的使用场景。

批量操作

批量操作在保持轻量的同时支持插入、更新和删除,但它没有提供结果的通知或回调。一个解决方案是开启持久化历史(Persistent History),这样我们可以得到批量操作的通知。至于回调,我们可以通过解析持久化历史来找出对应的变化。

storeDesc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)

接下来会详细解说各个批量操作。

批量插入

iOS 14 在 NSBatchInsertRequest 上新增了一套基于 Block 的批量插入 API:

// NSBatchInsertRequest.h

@available(iOS 13.0, *)
open class NSBatchInsertRequest : NSPersistentStoreRequest {
    open var resultType: NSBatchInsertRequestResultType

    // iOS 13 旧接口,通过数组批量插入
    public convenience init(entityName: String, objects dictionaries: [[String : Any]])
    public convenience init(entity: NSEntityDescription, objects dictionaries: [[String : Any]])

    // iOS 14 新增,通过 Block 批量插入
    @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)
}

举个🌰,不使用 Batch 逐个插入新对象:

// Earthquakes Sample - Regular Save

   for quakeData in quakesBatch {
             // 逐个创建 Entity
        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 { ... }

使用老的数组式 Batch Request 插入对象:

// Earthquakes Sample - Batch Insert

// 构建数组
var quakePropertiesArray = [[String:Any]]()
for quake in quakesBatch {
    quakePropertiesArray.append(quake.dictionary)
}

let batchInsert = NSBatchInsertRequest(entityName: "Quake", objects: quakePropertiesArray)

var insertResult : NSBatchInsertResult
do {
    insertResult = try taskContext.execute(batchInsert) as! NSBatchInsertResult
    ... 
}

使用 iOS 14 新加的 Block 式 Batch:

//Earthquakes Sample - Batch Insert with a block

var batchInsert = NSBatchInsertRequest(entityName: "Quake", dictionaryHandler: { 
    (dictionary) in
        if (blockCount == batchSize) {
              // 返回 true 表示结束
            return true
        } else {
            dictionary = quakesBatch[blockCount]
            blockCount += 1
        }
    })
    var insertResult : NSBatchInsertResult
    do {
        insertResult = try taskContext.execute(batchInsert) as! NSBatchInsertResult
        ...
    }

让我们来看看这三种方式在插入大量数据时性能上的差别:

方式 操作耗时 内存
无 Batch 62s 31M
iOS 13 数组式 Batch 30s 25.2 M
iOS 14 Block 式 Batch 11s 24.3 M

Block 式 Batch 无论在耗时还是在内存峰值上都有最好的表现。非 Batch 的耗时主要花费在合并数据更变 Notification 上了。

自动合并对象

在 Core Data 文件的 Entities 的 右侧 Core Data Inspector 编辑栏 ,将属性加入到 Constraints 后,就在这个属性上建立了一个 Unique 的约束。设置 mergePolicy 可以让 Core Data 自动支持 Model 更新。

managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

批量更新

NSBatchUpdateRequest 支持快速批量的更新数据,我们无需再经历查询、更新、保存的老流程。下面的代码将所有 magitute 大于 2.5 的数据的 validate 属性更新为 true

// 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
    ... 
}

批量删除

NSBatchDeleteRequest 支持:

  • 批量删除对象图中的大部分内容
  • 遵守对象关系。删除是级联的,关系也会被置空
  • 适用于清理过期对象或控制对象的 TTL(存活时间)

下面的代码在后台线程将删除所有 creationDate 在 30 天之前的数据:

```language-swift
// Batch Delete without and with a Fetch Limit
DispatchQueue.global(qos: .background).async {
moc.performAndWait { () -> Void in
do {
let expirationDate = Date.init().addingTimeInterval(-30243600)

       let request = NSFetchRequest<Quake>(entityName: "Quake")
top Created with Sketch.