大多数浏览器和
Developer App 均支持流媒体播放。
-
Core Data 杂项与准则
Core Data 是一种持久存储应用信息的核心方法,我们将向你展示如何优化实现这一功能,从而更快地获取数据。了解如何通过批量插入改进数据获取,根据你的数据需求定制获取请求,并对持久性存储中的更改通知作出反应。要想充分利用本节内容,你应该先了解并且曾经与Core Data进行过交互。若想了解更多有关该框架的信息,请观看“使用 Core Data 创建 app”。
资源
相关视频
WWDC19
-
下载
(你好 WWDC 2020) 你好 欢迎来到 WWDC (Core Data 杂项与准则) 大家好 我是来自 Core Data 团队的 Rishi Verma 本期视频中 我们会向大家展示 如何利用 Core Data 来更好地容纳 app 的各类需求 首先 我们来看一下如何快速有效地 用批量操作来充实并维护你的持久化存储 然后我们会讲解一下 如何根据 app 的需求 为其量身定制一个抓取请求 最后会为大家提供一些小建议和技巧 让大家了解 app 可以如何应对 持久化存储中的变更 首先我们来看看我们的示例 Earthquake Earthquake 是一个 Swift app 有一个视图环境来驱动用户界面 还有一个后台环境来 分析由美国地质调查局提供的数据 我们的样本为 app 设有一个本地容器 收集美国地质调查局 JSON 推送流 中的地震数据
我们把 JSON 推送流发送到 JSON 语法分析器 分析器随后会把数据发送到 我们的后台环境 将其转成地震管理对象 储存到本地中 我们的视图环境随后会把变更合并 像变魔术一样 我们的用户界面就会更新
我们的后台环境可能 会处理大量的管理对象 创建或抓取的这些对象的目的 是在存储之后马上丢弃 这里就是批量操作的优势所在 批量操作能够让开发人员 能轻松进行插入、更新和删除操作 且操作数也会维持在最低水平 正因如此 需要留心几个问题 不会提交保存通知 原因很明显 我们在这里并没有要保存 而且因为我们没有体现管理对象 也不会因为变更收到任何回调或访问逻辑 等一下 你可以通过导览与历史编辑纪录 绕过这两个问题 启用导览与历史编辑纪录 这样你的批量操作就会被抓取 这样就可以很轻松地获取一个通知 现在我们已经绕过了 第一个批量操作的问题 至于回调和访问逻辑 我们可以把相关变更的 导览与历史编辑纪录 解析到 app 的当前视图 我们再来深入了解一下批量操作怎么样? 我们的 app 首先要做的一般是 将数据载入持久化存储 然后驱动用户界面 我们在加入 NSBatchInsertRequest 时 它可以让开发者 能够进行数据注入的批量操作 简化了注入大量数据的能力 所以我们现在拓展了 NSBatchInsertRequest 的能力 最初 我们让开发者 有能力传递一个数组的字典 用于批量插入 这一个数组的字典代表的是要创建的对象 以键作为属性名称和赋值 我们还有一个新功能 一个能让开发者 用数据块块填充 一个特定字典或管理对象的初始器 大大降低了数据注入时的峰值内存 同时也进一步减少了对象分配的数量 我们来看一个例子怎么样?
在这个代码片段中 我们正在为地震创建管理对象
并用美国地质调查局提供的地震数据值 进行填充
然后保存 现在来看看 采用 NSBatchInsertRequest 之后的代码 首先我们收集了 一个包含所有地震数据的字典 然后添加到数组中
创建一个批量插入请求并执行
很简单 我们再来看看 这个批量插入请求的数据块变体 用可以给一个特定字典赋值的块 来创建批量插入请求 然后执行请求 这个数据块就被调用了 直到返回 true 的指示符之后 才停止并保存 但是这三种不同的方式如何运行? 我们来看看在我们注入大量地震时 app 会如何运行 在我们保存带有管理对象的上下文时 用时超过一分钟 释放出了大概 30 兆的内存 最初的峰值是 JSON 数据 一开始的一分钟 其中绝大部分时间不是用在注入上 而是合并变更通知 因为我们进行了大量的交易 但是我们没有批量插入的问题 我们在把 NSBatchInsert 与一个数组的字典一起使用时 我们用 25 兆的空闲内存 完成了相同的操作 还能在 13 秒内保存同样数量的对象 与传统的保存操作相比 所用的时间非常少 但我们并没有止步于此 我们来试试新的数据块注入功能 我们能在 11 秒内注入所有对象 既然我们现在已经优化了数据填充 接下来我们学习一个批量插入的小窍门 这是从 Earthquakes Sample 里 得到的管理对象模型 我现在已经选中了地震机构 右侧是数据模型检查器 用于我们的地震机构 我们再详细看一下 我们能看到“编号”属性是唯一性约束 这意味着在持久化存储中 只有一个对象能有一个特定“代码”值 其它地震不能用同一个“代码”值 这在我们的 Earthquakes 示例中 如何操作呢? 我们的 JSON 推送流向我们提供了 过去 30 天内发生的所有地震信息 而且 我们的用户每次点击重新载入按钮 我们就会从 JSON 推送流中注入所有数据 其中除了最近发生的几次地震 导致更新了相应的数据之外 大部分是相同数据 这里有一个地震编号是 42 是从 JSON 推送流中引入了分析程序中 然后它被传送到了我们的后台环境中 后台环境将 42 号地震进行了存储 数据第一次被注入时 我们会在存储中新建一行 但在后续插入相同的地震数据时 我们不想删除旧数据 然后插入新数据 我们想对发生更改的所有数据进行更新 这在 SQL 中称为 UPSERT UPSERT 是一个 SQL 词汇 如果你能同时看到 SQL 那会更容易理解这个词 所以 这里插入了我们的地震对象 插入的时候 如果编号存在冲突 那就不要插入 此时应该先更新这些属性 怎么才能做到这一点呢? 只需要在环境中设置合并策略 请求批量插入 NSMergeByPropertyObject- TrumpMergePolicy 但还有一个更简单的方法进行更新 最简单的方法就是批量更新 有了 NSBatchUpdateRequest 之后 没必要只为了更新管理对象并进行保存 去执行抓取需求 通过执行一个 NSBatchUpdateRequest 即可针对符合抓取需求中搜索标准的对象 快速、高效地更新其属性 我们迅速浏览一个例子 用我们的 Earthquakes 示例 app 如果我们能和其它信息来源确认这些地震 我们就可以把全部地震标记为已证实 我们假设信息来源只证实那些 2.5 级以上的地震
我们对此可以构建一个批量更新 该代码会为“地震”创建一个批量更新请求 然后在 propertiesToUpdate (即:要更新的属性)中 把 “validated” 设定为等于 “true” 并把谓词设定为等级大于 2.5
然后执行我们的批量更新 这样就算全部完成了 就是这么简单 我们已经讲了插入和更新 那现在讲一讲批量删除 批量删除的功能非常强大 可以用于轻松删除对象图的大部分内容 由于要遵守关联准则 所以删除会串级 关联会无效化 我们常见的一般使用场景是到期代码 到期代码决定了对象的存在期限 会将到期的对象清除 这是这个 API 中非常棒的一个功能 但看似简单的操作会有复杂的后果 我们举例说明一下? 在这个例子中 我们的地震数据会在 30 天后到期 由于这是一个清除任务 所以我们对其进行了异步调用 同时具备后台优先级 数据块在管理对象环境开始生效 而且决定了到期日期 然后我们构建抓取请求并设置到期谓词
用我们新的抓取请求 创建一个批量删除请求并执行
但如果我们有大量对象都符合 批量删除搜索标准 那该怎么办? 则抓取请求就会对我们的存储 执行一个编写锁
而锁定标准可能会没有时间限制 但不用担心 我们能解决这个问题 我们可以设置一个抓取限制 这样我们的任务就不会没有任何限定条件 还为用户减少了不少麻烦 既然我们已经避免了这个隐患 那我们来看看如何完善数据抓取方式 既然我们现在有这么详实的对象表 我们就需要调查并显示出存储的内容 在抓取数据时 我们取回的数据能够驱动 大量的视图和计算 但我们是每次都需要这么多数据吗? 我们如何在没有完整对象表的情况下 进行这些复杂的运算? 首先 managedObjectResultType 提供了最简单的方法 最大程度地将对象表进行转换 这个方法在我们用结果来批量处理 fetchResultsController 时很好用 当管理对象更新时 我们的 fetchResultsController 也会 响应并应用差异 我们在实操中看一下 这是没有数据的 Earthquakes 示例 现在我们来进行抓取 在对象被抓取时 fetchResultsController 在添加行数 并且当这些对象更新时 视图也更新了 但注意看这里 我们的视图显示了大约 15 个地震 可我们抓取的数量远不止这个数字 这里还有提升的空间 通过设置批量抓取请求的大小 结果只会将第一批给定数量的对象 完全填充数据 当用户上下滑动时 剩余对象将在用户需要的范围内 填充数据 批量数组的情况比较特殊 跟传统意义上的数组行为也不同 我来演示一下
这是一个常规的结果数组 所有管理对象均填充了数据 当对它们进行迭代时 一切都尽如预期 现在这个是一个批量的数组 注意 我们没有管理对象 但在对数组进行迭代时 ObjectIDs 将变成管理对象 在我继续进行下面的步骤时 批次将被释放 只保留 ObjectID 作为结果 让我们在实操中看一下 这是我们 Earthquakes 示范 抓取数据 它显示的空闲内存大约为 17 兆 如果我们打开批量抓取会怎么样呢?
当我们设置批量大小时 完成同样的任务只用了大约 12 兆的内存 差不多节省了 5 兆 大约占 app 内存使用的三分之一 还有什么其他方法来优化抓取呢?
我们可以减少抓取的数据量 若我们知道结果需要特定的属性或关联 我们可以根据这些要求来设定抓取 对于将要访问的已知属性 我们可以设置 propertiesToFetch 当我们处理管理对象时 默认行为是将关联设置为 “false” 且首个关联的转换 将触发关联对象的抓取 这在仅有几个或完全没有关联被转换 的情况下是好用的 但若已知一个关联极有可能被转换 我们建议将该关键路径设置为预抓取 以避免出现之后还需抓取数据的情况 并且由于每次出现否时都要加载各个转换 效率很低 这里是我们的抓取基线 空闲内存为 17.6 兆 但若我们将 propertiesToFetch 设置为仅在用户界面中可见的属性 我们能够将空闲内存减少到 16.4 兆
现在我们聊一下 ObjectIDs
管理对象很大且数据很多 但这些都不是理想的线程间传递 因此 ObjectIDResultType 就派上用场了 当我们想要完成这项工作 并且识别满足特定标准的对象时 这些简单的标识符 可以传递给其他线程做进一步处理 避免了处理线程上的查找成本 但如果我们需要介于完全管理对象 与 ObjectIDs 之间的东西呢? 比如像代码字典 因为它们提供的是能够传递给其他线程的 轻量级、只读数据集 因此非常方便 也可对代码字典结果做出定向调整 完成复杂的数据汇总 以此帮助减少庞大的计算 这种计算通常会要求拉入相关对象列表
比如在实体及其属性上 带有聚合功能的 groupBy 我们来看一个例子 按地点分列的平均震级 首先 我们确定震级的关键路径表达 之后确定功能表达来获取平均值 然后制作平均震级的表达描述
最后 为有属性抓取的地震设置抓取要求 以及表达描述和地点 按地点分组 并将结果设置为“代码字典” 这样做会带来这些结果 结果显示了指定区域中地震的平均震级 我们最后一个要讲解的结果类型是 countResultType 它简单、优雅、优化 因此十分棒 我们刚刚优化了注入和抓取 现在我们来看看如何优化 app 对持久化存储变化的反应 Core Data 有多种多样的通知 通知用户存储中进行了添加或删减 或是否有对象被保存或修改了 但我们想要着重看一下 两种特别好用的通知 今年 我们加入了 ObjectID 通知 因此 除了传统保存通知外 我们添加了一种新方式 ObjectID 通知与大家熟知的 管理对象保存通知相比更为轻量 这一便利的通知 是导览与历史编辑纪录交易提供的 让我们在 Swift 里看看新添加的功能 现在 Swift 中管理对象环境 有了一个现代化的通知 我们在 Swift 里 为一系列好用的老功能进行了更新 并添加了两个 能让大家通过 ObjectID 而不是管理对象驱动 app 的 新的通知 但我们的更新不止步于这里 为了进一步升级 我们还添加了通知键 让 Swift 中通知处理更为简单 我们还为 ObjectID 通知添加了新的键 我们想要聊一聊的另外一个通知 是远程变更通知
远程变更通知里面信息量很大 因为这类通知是由 Core Data 客户端 在流程内外进行的操作 而触发的 这就让你的 app 避免了轮询修改 并且能够用同样的逻辑驱动通知 当启用了导览与历史编辑纪录 远程修改通知的用户信息里面 包含了一个导览与历史编辑纪录令牌 这个令牌可用于获取 ObjectID 通知 来让我们看看具体实施
这是我们的容器和 app 和一个展示了 目前抓取的导览与历史编辑纪录表格 美国地质调查局的 JSON 推送流 是在线获取的 后台环境将 JSON 数据注入持久化存储
导览与历史编辑纪录 获取了操作的大量细节 比我们在这里展示给各位的多多了 之前我们的 app 需要 轮询存储以获得新修改 但有了远程修改通知就不用了 我们通知的是已进行的修改 而且远程修改通知的用户信息有效负载 有一个历史令牌 这样我能在持久化信息里面看到实际操作 让我们来看看 它是如何适应 app 的改变的 我们的 app 现在有了一些新功能 一个共享拓展 第二个采集同样数据的 app 和一个好用的 Photos 拓展 让其中任一新功能对持久性存储作出修改 该修改操作就会被记录 在导览与历史编辑纪录里 但是 当一个 app 转入后台 它需要轮询历史 这样消耗就很高
但如果我们启用了远程修改通知 当 app 在前台打开时就会收到通知
及接下来 Photos 扩展进行的修改
和我们的第二个 app
和我们的共享拓展
当 app 再次打开时 就会进行通知 我们可以轻松查看 对导览与历史编辑纪录进行了哪些修改 这两个功能就能让用户可以轻松查看到 持久性存储都被谁、什么时候、在哪 做出了什么修改和如何修改的 最后 一个关于 导览与历史编辑纪录的小技巧
导览与历史编辑纪录有一个好用的小技巧 和我之前告诉大家关于抓取请求的一样 要确保是根据 app 的需求进行请求的 这是一个如何根据需求请求的示例 可以让我们找到某一天后 对特定 ObjectID 进行的所有修改 首先 我们先获取 导览与历史编辑纪录修改对象的机构描述
这样可以用来构建抓取请求 和设置机构 我们在这里设置谓词 查找对某个特定 ObjectID 所做的修改 然后创建历史请求和设置抓取请求 各位请看 执行 结果将会是某个特定日期之后 指定 ObjectID 的修改 今天我们要讲的内容就是这些 快速总结一下 尽可能做批量处理、根据目的自定义抓取 借助通知和导览与历史编辑纪录的能力 感谢屏幕前的各位和 Core Data 团队 这是我的荣幸
-
-
1:48 - Batch Operations - Enable Persistent History
storeDesc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
-
2:32 - NSBatchInsertRequest.h
//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) }
-
3:01 - Earthquakes Sample - Regular 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 { ... }
-
3:16 - Earthquakes Sample - Batch Insert with Array of Dictionaries
//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 ... }
-
3:28 - Earthquakes Sample - Batch Insert with a block
//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 ... }
-
5:42 - NSBatchInsertRequest - UPSERT
let moc = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType) moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy insertResult = try moc.execute(insertRequest)
-
6:30 - Batch Update Example
//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 ... }
-
7:33 - Batch Delete without and with a Fetch Limit
// 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) } } }
-
12:18 - Fetch average magnitude of each place
//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
-
13:36 - NSManagedObjectContext.h - Modernized Notifications
//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 }
-
13:54 - NSManagedObjectContext.h - Modernized Keys
//NSManagedObjectContext.h @available(iOS 14.0, *) extension NSManagedObjectContext { public enum NotificationKey : String { case sourceContext case queryGeneration case invalidatedAllObjects case insertedObjects case updatedObjects case deletedObjects case refreshedObjects case invalidatedObjects case insertedObjectIDs case updatedObjectIDs case deletedObjectIDs case refreshedObjectIDs case invalidatedObjectIDs } }
-
14:08 - Enable Remote Change Notifications with Persistent History
storeDesc.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) storeDesc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
-
16:19 - History Pointers
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)
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。