4.2 加载并显示大型数据源
5 分钟阅读
原文链接: https://developer.apple.com/documentation/swiftui/loading-and-displaying-a-large-data-feed
4.2 加载并显示大型数据源
在后台消费数据,并通过批量导入和防止重复记录来降低内存占用。
概述
这个示例创建了一个应用,它通过消费美国地质调查局(USGS)的实时数据源,显示过去 30 天内记录到的美国地震列表。
要加载 USGS 的 JSON 数据源,请执行下列任一操作:
- 在 iOS 上,下拉刷新 List。
- 在 iOS 和 macOS 上,按下刷新按钮(⌘R)。
应用会在 URLSession 的默认委托队列上加载所请求的数据,那是一个在后台运行的操作队列。数据源下载完成、会话数据任务结束后,应用会继续在这个队列上把大量数据源元素导入存储,而不会阻塞主队列。
注意:这个示例代码项目与 WWDC21 的 10017:把 Core Data 并发带到 Swift 和 SwiftUI 场次相关。
在后台导入数据
要在后台导入数据,应用可以使用一到两个托管对象上下文。这个示例使用了两个(NSManagedObjectContext)实例:
- 一个主队列上下文,用来向用户界面提供数据。
- 一个私有队列上下文,用来在后台队列上执行导入。
两个上下文都连接到同一个 persistentStoreCoordinator。这种配置比使用嵌套上下文更高效。
示例通过 NSPersistentContainer 搭建 Core Data 栈来创建主队列上下文,后者会在其 viewContext 属性中初始化一个主队列上下文。
| |
调用持久化容器的 newBackgroundContext() 方法,创建一个私有队列上下文。
| |
数据源下载完成后,示例用任务上下文在后台消费该数据源。在 Core Data 中,每个基于队列的上下文都有自己的串行队列,应用必须把操作该上下文的任务与队列串行化,做法是用 perform(_:)(带或不带 await 关键字)或 performAndWait(_:) 闭包把代码包起来。
| |
关于并发的更多信息,参见 NSManagedObjectContext。
为高效处理大型数据集,示例使用 NSBatchInsertRequest,它直接访问存储——不与上下文交互、不触发任何键值观察,也不分配托管对象。NSBatchInsertRequest 的闭包式构造器让应用可以在 Core Data 调用 dictionaryHandler 闭包时一次提供一条记录,这有助于应用保持较低的内存占用,因为无需为所有记录准备缓冲区。
| |
合并改动并更新用户界面
由于 NSBatchInsertRequest 绕过上下文、不会触发 NSManagedObjectContextDidSaveNotification 通知,需要用改动更新界面的应用有两种选择:
- 通过解析存储的持久化历史提取相关改动,再把它们合并到视图上下文中。关于持久化历史跟踪的更多信息,参见消费相关的存储改动。
- 从存储重新获取数据。不过,如果视图上下文被固定到某个查询代(query generation),就需要在获取数据之前重置该上下文。关于查询代的更多信息,参见在存储变化时访问数据。
这个示例用持久化存储远程变更通知和持久化历史跟踪来更新界面,原因是:
- 数据模型只包含一个实体,因此所有改动都与
List相关,不需要解析历史中的具体改动。 - FetchRequest 直接从存储获取并取出结果,
List会自动刷新其内容。 - SwiftUI 只关心视图上下文,因此
QuakesProvider观察 NSPersistentStoreRemoteChange 通知,把后台上下文(执行批量操作的那个)的改动合并到视图上下文中。
把存储描述的 NSPersistentStoreRemoteChangeNotificationPostOptionKey 选项设为 true,即可为持久化存储启用远程变更通知。
| |
把 NSPersistentHistoryTrackingKey 选项也设为 true,即可为持久化存储启用持久化历史跟踪。
| |
每当持久化存储内发生改动——包括其他进程的写入——存储都会发出远程变更通知。示例收到该通知后,会获取在给定令牌之后发生的持久化历史事务和改动。持久化历史变更请求取出历史之后,示例把每个事务的 objectIDNotification() 通过 mergeChanges(fromContextDidSave:) 合并到视图上下文中。
| |
每执行完一个 NSBatchInsertRequest 或 NSBatchDeleteRequest,示例都会把任何界面更新派发回主队列,以便在 SwiftUI 中渲染。
| |
合并完最后一个事务的改动之后,示例需要把该令牌存到内存或磁盘上,以便在后续的持久化历史变更请求中使用。
分批处理以降低内存占用
当应用在上下文中获取或创建对象时,Core Data 会缓存该对象,以免应用再次使用这些对象时还要往返存储文件。然而,随着处理的对象越来越多,这种做法会增大应用的内存占用,最终可能导致 iOS 上的低内存警告或应用被终止。NSBatchInsertRequest 不会明显增加应用的内存占用,因为它不会把数据加载到内存中。
注意:需要在早于 iOS 13 或 macOS 10.15 的系统上运行的应用,必须通过分批处理对象、并在每批之后调用 reset() 重置上下文,来避免内存占用增长。
示例把 viewContext 的 automaticallyMergesChangesFromParent 属性设为 false,以免 Core Data 在每次后台上下文存储时都自动合并改动。
| |
防止存储中出现重复数据
示例应用每次重新加载 JSON 数据源时,解析出的数据都包含过去一个月的全部地震记录,因此可能与已导入的数据大量重复。为避免创建重复记录,应用约束某个属性、或属性组合,使其在所有实例中唯一。
code 属性唯一标识一条地震记录,因此在 Quake 实体上约束 code,可以确保没有两条已存储记录的 code 值相同。
在数据模型编辑器中选中 Quake 实体。在数据模型检查器中,点击 Constraints 列表下方的 + 按钮添加一个新约束。此时会出现一个约束占位符。
| |
双击该占位符进行编辑。输入作为该实体唯一约束的属性名,或者以逗号分隔的属性列表。
| |
存储新记录时,存储会检查是否已存在被约束属性取值相同的记录。发生冲突时,会启用 NSMergeByPropertyObjectTrumpMergePolicy 策略,由新记录覆盖已有记录的所有字段。
| |