大多数浏览器和
Developer App 均支持流媒体播放。
-
利用 Core Data 构建 App
Core Data 可以帮助管理您整个 app 中的数据流。了解 Core Data 中让您的代码变得更简洁、更强大的新功能,例如派生属性、历史记录跟踪、更改通知和批处理操作等。进一步了解如何使用 UIKit 和 Foundation 中的这些功能和新比较 API 来提高您 app 的运行效率。
资源
相关视频
WWDC20
WWDC19
-
下载
(使用Core Data 制作app) 大家好
欢迎来到“通过Core Data 做app”演讲 我是Scott Perry 来自Core Data团队 今天 我们要做一次快速回顾 重点是最佳实践 主题有建立和运行 Core Data 设置app控制器 以及使用多个协调器 和相关缩放 最后总结一些有用的测试技巧
现在开始吧 今年发布了一个新的样本app 演讲202中已经演示过了 从发布列表开始
点击上方+ 可以添加新发布 仔细看下细节视图 这是类似于博客的app 支持题目 内容 多个标签 以及多媒体附件 就像这个缩略图
我们改一下标题 再添加一个标签
你好
好了 保存 然后回到列表视图
现在看看标签管理器 如你所见 这里只有一个标签示例 和三个发布 我们再加两个 一个猫咪 一个狗狗
现在退出 打开小工具栏 选择添加1000个随机发布 好了
再回到标签列表 可以看到狗狗比猫咪更受欢迎 这是科学
以上就是app的样子 下面来想想数据的结构 最明显的是post类型 所以它是起点 我们还支持多媒体附件 就是下一个类型 同理还需要标签类型 但是附件可能很大 所以要将数据分别保存 毕竟列表视图只显示缩略图 大件数据可以存到别的地方
数据结构就组织好了 它可以很直观地转换成 Core Data模型 用Model Editor 和Xcode 这里定义了模型类型的组成 以及它们的关系 比如附件和对应数据的关系 是一对一 就是说一个附件背后 只能有一个图像数据 一个图像数据也只能代表一个附件
模型中可以告诉Core Data 如果附件被删除 图像数据也要自动清除 这就是串联删除规则
双箭头表示一对多的关系 一个post可能有多个附件 但一个附件只能属于一个post 最后 post可能带多个标签 标签可以加到不同的post中 这是多对多的关系
以上就定义了一个托管对象模型 但我们还要了解 更多的Core Data类型 在搭建app之前 模型通过PersistentStoreCoordinator调用 如字面意思 它负责管理持久化存储 通常 它是存在文件系统的数据库 但它可以一次存储多个存储 包括NSPersistentStore 驱动的自定义类型
最后花费时间最多的类型 是ManagedObjectContext Core Data使用命令模式 说白了就是 需要上下文完成任务 如果请求fetch 就需要Context 通过ManagedObject Context收到后会存为命令 Context 需要有协调器才能工作 如我之前所说 协调器需要知道模型的存在 才能理解存储 各个类型相互依赖 但协调器提供的一个类型可囊括所有 并代表整个堆栈 叫做持久化容器 通过持久化容器 用几行代码就能搭建一个堆栈 特别是当模型在bundle中时
我们要做的是通过名字引用它 持久化容器就会帮我们加载
如果通过代码生成模型 或需要带多个容器的同一模型 这个初始化器能给我们控制权
有了容器 我们就告诉它加载持久化存储 每次存储调用一次 completionBlock error参数为空则成功 现在让我们聚焦托管对象上下文
Context提供 对托管数据的无缝访问 有些选项可以让它们更有用 对某些使用案例 比如视图驱动
第一个是查询生成 查询生成提供存储数据的稳定视图 可以安全且持续地访问对象 即使被其他actor修改或删除
为此 设context的查询 生成为current current是一个浮动元 context的查询生成会固定到 一个具体的时间点 在访问存储的下一步中
现在上下文在存储中 有了稳定的数据视图 如果要更新到最新修改怎么办? 在以前 要注册上下文 然后保存通知 但上下文也可以被设置为 在sibling保存修改后 自动更新
automaticallyMergesChangesFromParent 为true
现在所有堆栈类型都已经设置好 怎么在app中使用呢? 最重要的是记住 在使用上下文时 所有对托管对象的存储请求和互动 都要在上下文队列中完成 背景上下文都单独的队列 可以用它们执行API 这里有个blocking不变量 和异步版本 对真正的异步工作 容器会为执行后台任务提供便利 为你创建后台上下文 在block返回后自动丢弃
那么如何给app添加数据? 如果我们只需要几个对象 最简单的是 用托管对象子类提供的初始化程序 由Xcode下的模型生成 如果子类只有一个模型使用 它就知道代表哪个实体 所以可以带上下文使用 带context注册后 就可以进行配置 跟其他实例变量一样
配置好以后 调用context.save 把它存入存储
需要更多数据怎么办? 大多app并不满足于 一次只导入一部分对象 好机会 因为你有服务器组件 返回成百上千的对象 我们可以一次存一个 就像刚才做的那样 但这要写很多样板代码 更不用说大量的资源开销 一次把所有放进内存
批量插入 这是今天要讲的第一个新功能 这个代码片段有1000个对象 都基于反序列化负载 形式为带字符串密钥的字典数组
这些密钥要对应 模型中的属性名称 也可以留一些 如果不需要的话 比如唯一的限制 如果模型已经配置默认值 Core Data就会用它们 这里有三个字典 每个覆盖不同的内容和标题属性
回到代码 用字典数组创建批量插入请求 以及对象模型实体 它也可以采用托管对象子类 通过Xcode生成
执行请求 然后上下文会回复 一个布尔值 告诉我们操作是否成功
很多都免费 你一定在想 区别是什么 这里实际上并没有什么明确的界限 它们大多听着都类似 如果你用过批量更新或批量删除
如果有唯一限制 任何现有的对象 匹配字典的 都会从数据库中拿出 更新为新的值
可选的或设置为默认值的属性 也可以从字典中删除 在用唯一限制更新对象时 现有值不会被更改
这对关系也适用 批量插入不能用于设置关系 但如果批量插入在现有对象中更新 由于唯一限制 现有关系不会被修改 最后 与其他批量操作一样 批量插入不会生成 contextDidSaveNotification 所以你要自己完成 有了数据后 我们讲讲如何满足 控制器的需求 控制器要做的第一件事 是抓取和显示数据 这就来看看
通过fetchRequest 从存储中获取对象 就像创建新对象时获取实体一样 托管对象子类有个方法 由Xcode生成 提供配置好的 fetchRequest
添加predicate给 request可以优化结果 这里就能通过名称抓取标签
执行请求后 获取结果并用来配置视图
这也适用于不可变数据 如果在视图打开时 标签名称或颜色改了怎么办 托管对象上下文 会确保更新对象的属性 但我们还没有深入 观察这些修改
如你们所知 托管对象支持关键值观察 一直以来都是 但是本周宣布的组合框架 让在Swift中使用KVO更简单 从对象中给每个要布线的属性 一个publisher 然后将数据流同步到视图的 assignment subscriber 对于标签颜色 要多做一步映射 对应各个类型
这就是所有要写的代码 这样视图内容就会自动更新 随着底层对象的修改
联合框架很优秀 但这也只是冰山一角 框架的表面 它还提供了其他更多 有用的数据流功能 欲了解更多 请观看演讲721
你可能注意到 代码中有个小骗局 就是细节视图 几乎从不真正抓取对象 显示出来 通常这种视图 会从配置为托管对象的母程序中推出 因此通常我们写的代码是这样的
细节视图的母程序 通常是列表视图 比如集合或表格视图 它也通过抓取请求获取对象 但我们还有几个属性没有讲到 是抓取多个对象的关键
首先是 Request.SortDescriptors 它定义了结果的顺序 这里要排序的是标签名称 但如果名称区别不大 就要在数组末尾加 另一个descriptor 区分开来
另一个有用的选项 是Batching 当抓取请求匹配了1400万个对象 我们不能把所有都放进内存 在同一时间 即使内存有个空间 当然一般没有 也会花很多时间 因此 设置一个 fetchBatchSize 告诉上下文 一次要向存储载入多少对象 这对app的响应影响很大 但它只能用于交互 NS数组结果 Swift的数组桥接会盖过优化
记住这一点 就很容易能设置 fetchRequest
抓取所有需要的标签 但如果其中一个变了呢
就像细节视图中显示的对象属性 需要保持对象跟随查询变化而变化
好在Core Data支持即时请求 FetchResultsController 这里配置的请求是 抓取按名称排序的所有发布 一次50个
这个请求连接一个context 设置的类型是我们还没讲到的 fetchedResultsController 它将修改传达给 fetchRequest 通过代理协议 并立刻运行 在发出执行fetch命令后
FetchedResultsController 你们可能都知道了 代理协议修改报告回调 这个方法说明修改何时发生 这一个说明 section变化的时间 这一个说明每个修改的对象及其变化 最后这个说明任务完成 一切了如指掌 这些方法旨在紧紧对应 UITableView的API 但还要写许多粘合代码 匹配请求结果给重新绘表 另外 新的集合视图 不支持这个修改回调
但好消息是 FetchedResultsController 今年学会了新技能 首先是代理方法 发布NSDiffableDataSourceSnapshot实例
听起来很多人都已经知道它是什么了 要是不知道 那它是新的 UIKit MapKit类 代表集合视图 或TableView的结构 为此 设置 TableView的数据源 为DiffableDataSource类 这也是今年的新东西 填入快照 你可以在演讲220中了解更多 但对于我们 你只要知道 它们可以用来更新集合视图 只用一行代码 这段代码放在这页幻灯片的 下半部分刚刚好 本来需要很多样板代码 这里要做的 是将快照放入 DiffableDataSource 数据源将快照区分于 之前的视图状态 也就是 DiffableDataSource 然后用计算机差异更新视图
等了很久了
DiffableDataSourceSnapshot 对整个集合视图的批量控制很有用 但是如果需要自定义 或需要管理的是除Table和 Collection视图外的类型 你可能需要另一个代理方法 对于Snapshots 它总结了所有对 FetchedResult的修改 用一张快照 但用的是不同类型 今年新出的 CollectionDifference
如字面意思 CollectionDifference 对差别编码 对比两套集合而生成
Swift标准库里的 Collection diffing 在Swift Evolution Proposal 240中引入 Foundation中也有 演讲711中有更详细的信息
在这里 它是个一维类型 只在不使用section 抓取时可用 就像SnapshotDelegate方法 与遗留修改报告方法相互排斥 如果要驱动 FetchedResultsController的多个内容 要用多个 FetchedResultsController 现在快速展示下 如何驱动集合视图上的一个部分 利用这些差异 这是代理方法 从最上面的批量更新开始 遍历diff中的修改
CollectionDifferences 支持两种修改 插入和删除 这两种相反的类 在关联中可能会互相引用 第一个例子中 一个插入关联一个删除 就是说对象被删除了 或至少被修改了 逻辑位置可能还没移走 但不要紧 因为CollectionView 很聪明 能看出区别 只要有正确的原值 和目标IndexPaths 就可以了 第二部分 插入了一个对象 之前不属于 Fetched Results 所以让CollectionView 去添加
最后 对所有不属于关联移动的 删除进行匹配 通过筛选新的关联 然后从CollectionView中 移除它们 就这样 你看这个代码 结构很好 因此它可以作为函数因子的备选 进一步减少样板代码 因为你只要有 CollectionView和差别就行 我想人们一定会爱上 这些新的代理方法 今年 旧的方法也有了新生 Swift UI框架 引入了第一方支持 给描述性接口 但不被Snapshot 或diff驱动 这个模型类别可衍生自 Fetched Result列表 现有的控制器 didChangeContentDelegate方法 告知Fetched Result 的每次更新 让我们准备更新视图
将Fetched Result 应用到视图不难了 但如果结果本身就很难抓取呢? 如果最后不能建立抓取请求呢? 如果运行出现了问题 在执行抓取请求的时候? 有些时候 控制器要求大过模型要求 只能放弃一些模型纯度 为了达成目标 这就要反范式化 WWDC 2018的 演讲224中有讲过 回顾一下 反范式化 指当我们保存数据副本 为方便访问时 会出现额外的负载 维护额外的数据 很多时候这种权衡很容易 数据库索引就是很好的例子 我们不再保存所有索引列的副本 而是通过闪电般的请求 当它们依存于这些索引时 回到app 反范式化还能帮助我们 追踪每个标签下的发布数
我们只要添加一个 整数属性给标签类型 名字为postCount 现在只要保证这个新属性 在每次发布被标签时递增 在每次发布删除标签时递减 这个代码肯定不会有漏洞 数据也会永远保持一致对吧
对吗
不对 所以要讲讲派生属性
派生数据是范式元数据 由Core Data维护 不仅是为了计数 它还有很多功能 文档中都能看到 派生属性在托管对象模型中定义 Xcode中的模型编辑器 为此开了一个新接口 你可以用代码定义派生属性 使用新类型 NSDerivedAttributeDescription
最后 派生表达式可以指向 下一层的一个实体的任何属性
派生属性让反范式化十分简单 现在我要展示一下如何应用它们
这是之前那个app 打开这个标签管理 看到三个标签和一堆发布 但驱动它的视图代码 实际上遍历了关系发布 获取计数 这是关系中默认的 如果有更多数据的重要性排序 它可能会导致运行出问题 我们要在模型中用派生属性修正
这是模型编辑器 我们看到的是标签类型 我们要在这里添加 新属性postCount 并设为整数类
现在看到这里的查看器 多了一个新选项derived 勾选它 出现一个框 填写衍生表达式 现在我们是将代码 从视图转移到了这里 写入posts.@counts 因为用的聚合函数
就可以了 如果重建 Xcode会重建托管对象子类 回到这个视图 会看到新的实例变量 postCount 但它不是可选的 所以要删除更多代码 避免这种情况 搭建并运行后 没有任何变化
只是更快更好了 我们不用更新所有内容 果然 就是这样
开发者文档中 可以找到完整的派生属性集 通常来说 它们分为四类 最简单的是全部复制 就像复制附件标识符 及其图像数据 另一种派生是字段转换 比如小写标签名称 或规范化某些Unicode字符串
刚才的例子里 一个聚合函数 在演示中跨过了多个关系
最后是全局函数 不用任何参数 可用于 比如 追踪对象最后更新的时间 既然控制器都讲完了 下面来看更高级的主题和缩放 PersistentHistory 是2017年引进的工具 处理importer 添加的数据 或维护多个活跃协调器的数据一致性 使用同一存储 今年新的是 我们用抓取请求 让你只查看需要的历史数据
比如你要查看一个扩展做的修改 或者只看影响发布的修改 或只看某个时间段的历史 想象将抓取请求的力量 用到PersistentHistory
NSPersistentHistoryTransaction 和NSPersistentHistoryChange 不是模型化类型 但它们能生成新的交叉方法 与fetchRequests交互 注意 新的方法与Xcode生成的很像 就是给托管对象子类的 它们包括了 entityDescription的附件 对应类型 以及产生新的预置 fetchRequest的方法 返回类型实例 NSPersistentHistoryTransaction 或NSPersistentHistoryChange 在执行时 配置fetchRequest 给predicate 跟抓取托管对象一样 但是不再执行 fetchRequest 直接作用于上下文 而是作为 PersistentHistoryRequest的一部分
NSPersistentHistoryChangeRequest 有新的初始程序 用于创建 fetchRequest的新实例 以及用于配置 post-talk的不可变属性 管理PersistentHistory 的fetchRequest 为app增加了粒度 但还有个问题 我们怎么知道历史在什么时候发生?
这有很多方法 但都有缺陷 我们可以对修改进行存储 但这很难调整解决方案 平衡徒劳无功 和在修改被发现前出现的延迟 文件系统监控系统 比如 Dispatch Sources 和FSEvents也有用 但它们只用于文档后备存储 它们不容易应用 还会出现很多通知 并不能对应存储的变化 因此还是会做很多无用功
虽然跟创可贴一样有用 但它们都不是最佳解决方案 解决其他协调器修改存储时 通知的问题
今年有新的通知功能 可以当作是跨协调器保存通知 但事件是异步传达的 所以更像是跨协调器更改通知 我们叫做远程修改通知
要启用远程修改通知 用新的 PersistentStore 叫做 NSPersistentStoreRemoteChangeNotificationPostOptionKey
设置它为存储描述 然后加载 PersistentStore到协调器 然后协调器就会听取 对存储做出的远程修改 它还告诉协调器 它要发送远程修改通知 在变化发生的时候 如果要修改通知 就要用 NSPersistentStoreRemoteChangeNotificationPostOptionKey 给所有的协调器 不仅是给要应用修改的协调器 可能还要开启 PersistentHistory 因为 虽然远程修改通知 可以告诉你哪个存储被改了 通过NSPersistentStoreURLkey 在notification.UserInfo中 但如果开启了 PersistentHistory 它还会包括该事务创建的 新的历史令牌 它会十分有用 特别是当结合 新的PersistentHistory 抓取功能时 我们刚刚讲过的
远程修改通知类似于推送通知 有时候修改会让它们一起崩溃 如果一下子很多的话 只有最有一个修改能被传达 如果随意启动app 怎么知道哪个是当前的 PersistentHistory令牌 新方法就是 PersistentStoreCoordinator
叫做CurrentPersistentHistoryToken 集合这些新东西在一起 就能解开 过去的未解之谜 我们不仅能及时更新 PersistentHistory 还能只关注 影响到工作的修改
我想最好的讲解方法 是做个演示 看看app里的样子
这是个控制器 负责让视图上下文保持最新 无论协调器何时改变 storeRemoteChange 已经设为 在收到远程修改通知时调用
打开后可以获得新的 PersistentHistory令牌 保存在userInfo之外 然后抓取要更新的上下文 进入perform block
首先 要将 PersistentHistory 减到需要的内容 根据上一个历史令牌 创建predicate 在这个app的例子里 是当前的 PersistentHistory令牌 然后将所有事务前置 或等同于 通知给的新历史令牌
如果使用transaction author 应该用 可以创建一个predicate 允许我们查看仅限于 其他协调器创建的事务
然后创建 historyFetchedRequest 获取事务 设置predicate为 CompoundPredicate 对刚创建的所有predicate 现在看到的事务就仅限于 其他协调器编写的 夹在之前看到的 和刚包含的新的之间
创建NSPersistentHistoryChangeRequest 设置为刚创建的 fetchedRequest 然后对上下文执行
结果是一个未更新事务的列表 对每个来说 可以将 objectIDNotificationuserInfo 传递给 ManagedObjectContext.mergeChanges fromRemoteContextSave 2017年引入 所有事务处理完毕后 视图就与最新的磁盘数据保持一致了 这几乎是实时完成的 因为推送在本地 速度很快
最后 为确保下一次不会做无用功 要记住最后处理的 PersistentHistory令牌 现在就能继续下一个远程修改通知了
好了 以上就是 PersistentHistory抓取 和远程修改通知
最后 我要讲一下测试 大家可能会运行测试 用多种配置 比如 sanitizer和发布 我想针对Core Data 提点建议
第一是要了解运行目标 联系人app会要测试 至少几万个对象 但对于这个运行目标 可能实际要在桌面硬件上 使用线性算法 有的时候
图库app就不同 它的测试是要验证所有工作都正确 因为有几百万个对象 这样的规模下 任何对数时间的延迟 都能卡顿app 因此重要的是 飞速测试 运行所有带性能数据集的集成测试
集成测试也要运行优化配置 用于测试和显示其他问题 使用Core Data的 app应该 利用框架内置的并发调式 Scheme Editor中开启 设置com.apple.CoreData.ConcurrencyDebug 1 在过程参数列表中
在多个配置中运行测试 会很费时 同样 单元测试也要尽可能快 当测试运行时间为关键时 用内存存储是个不错的办法 这里单指 SqLightStores内存模式 SqLightStore 一直支持内存模式 通过它 在创建持续化容器和加载存储之间 可以拉取存储描述 将URL属性设为指向 dev/null
这会创建一个极为性能化的堆栈 但内存存储不能在协调器之间共享 为了验证远程修改通知的工作 刚刚完成的 要利用命名的内存存储 命名的内存存储 通过在 dev/null后添加路径组件而定义 同一进程中的任何带URL的 其他SqLightStore 会连接到共享的内存数据库
共享一个内存存储的不同的协调器 会给彼此分发远程修改通知 让我们能测试所有逻辑
最后 用sanitizer 每个至少省了一次 我见过 address sanitizer 能立刻识别一个字节的缓冲溢出 以前可能要几个月才能隔离 同样 thread sanitizer 能理解临界段代码 能告诉你内部无法复制的线程痕迹 最后UBSan可以 在漏洞出现前解决它们 通过识别未来可能有变化的 未定义行为 今天我们讲了如何用 Core Data配置 如何利用新的和旧的API编写 模型和控制器之间 无缝、紧凑、稳健的集成 我们探索了多个协调器之间的同步 并对此定义需求和测试代码 我们变得更自信 我们乐于解你的想法 请通过反馈助手告诉我们 更多信息请登录开发者官网 本场演讲的网页上有今天提到过的 其他演讲的链接 明天的实验室见 谢谢参与
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。