Core Data with CloudKit (一) —— 基础

在 WWDC 2019 上,苹果为Core Data带了一项重大的更新——引入了NSPersistentCloudKitContainer。这意味着无需编写大量代码,使用Core Data with CloudKit可以让用户在他所有的苹果设备上无缝访问应用程序中的数据。

Core Data为开发具有结构化数据的应用程序提供了强大的对象图管理功能。CloudKit 允许用户在登录其 iCloud 账户的每台设备上访问他们的数据,同时提供一个始终可用的备份服务。Core Data with CloudKit则结合了本地持久化+云备份和网络分发的优点。

2020 年、2021 年,苹果持续对Core Data with CloudKit进行了强化,在最初仅支持私有数据库同步的基础上,添加了公有数据库同步以及共享数据库同步的功能。

我将通过几篇博文介绍Core Data with CloudKit的用法、调试技巧、控制台设置并尝试更深入地研究其同步机制。

Core Data with CloudKit (一) —— 基础

Core Data with CloudKit(二) —— 同步本地数据库到 iCloud 私有数据库

Core Data with CloudKit(三)—— CloudKit 仪表台

Core Data with CloudKit(四)—— 调试、测试、迁移及其他

Core Data with CloudKit(五)—— 同步公共数据库

Core Data with CloudKit (六) —— 创建与多个 iCloud 用户共享数据的应用

健康笔记 - 全家人的健康助手

健康笔记适用于任何有健康管理需求的人士。提供了强大的自定义数据类型功能,可以记录生活中绝大多数的健康项目数据。你可以为每个家庭成员创建各自的记录笔记,或者针对某个特定项目、特定时期创建对应的笔记。

推荐

Core Data with CloudKit 的局限性

  • 只能运行在苹果的生态

不同于其他的跨平台解决方案,Core Data with CloudKit只能运行于苹果生态中,并且只能为苹果生态的用户提供服务。

  • 测试门槛较高

需要有一个 Apple Developer Program 账号才能在开发过程中访问CloudKit服务和开发团队的CKContainer。另外,在模拟器上的运行效果也远没有在真机上可靠。

Core Data with CloudKit 的优点

  • 几乎免费

开发者基本上不需要为网络服务再额外支付费用。私有数据库保存在用户个人的 iCloud 空间中,公共数据库的容量会随着应用程序使用者的增加而自动提高,最高可增加到 1 PB 存储、10 TB 数据库存储,以及每天 200 TB 流量。之所以说几乎免费,毕竟苹果会扣取 15-30%的 app 收益。

  • 安全

一方面苹果通过沙盒容器、数据库区隔、加密字段、鉴权等多种技术手段保证了用户的数据安全。另一方面,鉴于苹果长期以来在用户中树立的隐私捍卫者的形象,使用Core Dat with CloudKit可以让用户对你的应用程序增加更多的信任。

事实上,正是在 WWDC2019 年看到这个功能后,我才有了开发 【健康笔记】 的原动力——既保证数据隐私又能长久的保存数据。

  • 集成度高、用户感知好

鉴权、分发等都是无感的。用户不需要进行任何额外的登录便可享受全部的功能。

Core Data

Core Data诞生于 2005 年,它的前身EOF在 1994 年便已经获得的不少用户的认可。经过了多年的演进,Core Data已经发展的相当成熟。作为对象图和持久化框架,几乎每个教程都会告诉你,不要把它当作数据库,也不要把它当作ORM

Core Data的功能包括但不限于:管理序列化版本、管理对象生命周期、对象图管理、SQL 隔离、处理变更、持久化数据、数据内存优化以及数据查询等。

Core Data提供的功能繁多,但对于初学者并不十分友好,拥有陡峭的学习曲线。最近几年苹果也注意到了这个问题,通过添加PersistentContainer极大的降低了Stack创建的难度;SwiftUICore Data 模版的出现让初学者也可以较轻松地在项目中使用其强大的功能了。

CloudKit

在苹果推出iCloud之后的几年中,开发者都无法将自己的应用程序同iCloud结合起来。这个问题直到 2014 年苹果推出了CloudKit框架后才得到解决。

CloudKit是数据库、文件存储、用户认证系统的集合服务,提供了在应用程序和iCloud 容器之间的移动数据接口。用户可以在多个设备上访问保存在iCloud上的数据。

CloudKit的数据类型、内在逻辑和Core Data有很大的不同,需要做一些妥协或处理才能将两者的数据对象进行转换。事实上,当CloudKit一经推出,开发者就强烈希望两者之间能够进行便捷的转换。在推出Core Data with CloudKit之前,已经有第三方的开发者提供了将Core Data或其他数据的对象(比如realm)同步到CloudKit的解决方案,这些方案中的大多数目前仍在提供支持。

依赖于之前推出的 持久化历史追踪 功能,苹果终于在 2019 年提供了自己的解决方案Core Data with CloudKit

Core Data 对象 vs CloudKit 对象

两个框架都有各自的基础对象类型,相互之间并不能被一一对应。在此仅对本文涉及的一些基础对象类型做简单的介绍和比较:

  • NSPersistentContainer vs CKContainer

NSPersistentContainer通过处理托管对象模型(NSManagedObjectModel),对持久性协调器(NSPersistentStoreCoordinator)和托管对象上下文(NSManagedObjectContext)进行统一的创建和管理。开发者通过代码创建其的实例。

CKContainer则和应用程序的沙盒逻辑类似,在其中可以保存结构化数据、文件等多种资源。每个使用CloudKit的应用程序应有一个属于自己的CKContainer(通过配置,一个应用程序可以对应多个CKContainer,一个CKContainer 也可以服务于多个应用程序)。开发者通常不会在代码中直接创建新的CKConttainer,一般通过iCoud 控制台或在Xcode TargetSigning&Capabilities中创建。

  • NSPersistentStore vs CKDatabase/CkRecordZone

NSPersistentStore是所有 Core Data 持久存储的抽象基类,支持四种持久化的类型(SQLiteBinaryXMLIn-Memory)。在一个NSPersistentContainer中,通过声明多个的NSPersistentStoreDescription,可以持有多个NSPersistentStore 实例(可以是不同的类型)。NSPersistentStore没有用户鉴权的概念,但可以设置只读或读写两种模式。由于Core Data with CloudKit需要 持久化历史追踪 的支持,因此只能同步将SQLite作为存储类型的NSPersistentStore,在设备上,该NSPersistentStore 的实例将指向一个SQLite 数据库文件

CloudKit上,结构化的数据存储只有一种类型,但采用了两个维度对数据进行了区分。

从用户鉴权角度,CKDatabase分别提供了三种形式的数据库:私有数据库、公有数据库、共享数据库。应用程序的使用者(已经登录了iCloud账号)只能访问自己的私有数据库,该数据库的数据保存在用户个人的iCloud空间中,其他人都不可以对其数据进行操作。在公共数据库中保存的数据可以被任何授权过的应用程序调用,即使 app 的使用者没有登录iCloud账户,应用程序仍然可以读取其中的内容。应用程序的使用者,可以将部分数据共享给其他的同一个app的使用者,共享的数据将被放置在共享数据库中,共享者可以设置其他用户对于数据的读写权限。

数据在CKDatabase中也不是以零散的方式放置在一起的,它们被放置在指定的RecordZone中。我们可以在私有数据库中创建任意多的Zone(公共数据库和共享数据库只支持默认Zone)。当CKContainer被创建后,每种数据库中都会默认生成一个名为_defaultZoneCKRecordZone

因此,当我们保存数据到 CloudKit 数据库时,不仅需要指明数据库(私有、公有、共享)类型,同时也需要标明具体的zoneID(当保存到_defaultZone时无需标记)。

  • NSManagedObjectModel vs Schema

NSManagedObjectModel是托管对象模型,标示着Core Data对应的数据实体(Enities)。绝大多数情况下,开发者都是使用XcodeData Model Editor来对其进行的定义,定义会被保存在xcdatamodeled文件中,其中包含了实体属性、关系、索引、约束、校验、配置等等信息。

当在应用程序中启用CloudKit后,将在CKContainer创建一个SchemaSchema中包括记录类型(Record Type)、记录类型类型之间可能存在的关系、索引以及用户权限。

除了直接在iCloud控制台创建Schema的内容外,也可以通过在代码中创建CKRecord,让CloudKit自动为我们创建或更新Schema中对应的内容。

Schema中有权限的设定(Security Roles),可以分别为worldicloud以及creator设定不同的读写权限。

  • Entities vs Record Types

尽管我们通常会强调Core Data不是数据库,但实体(Enitities)与数据库中的表非常相似。我们在实体中描述对象,包括其名称、属性和关系。最终将其描述成NSEntityDescription并汇总到NSManagedObjectModel中。

CloudKit中用Record Types描述数据对象的名称、属性。

Enitiy中有大量的信息可以配置,但Record Types只能对应描述其中的一部分。由于两方无法一一对应,因此在设计Core Data with CloudKit的数据对象时要遵守相关规定(具体规定将在下一篇文章中探讨)。

  • Managed Object vs CKRecord

托管对象(Managed Object)是表示持久存储记录的模型对象。托管对象是NSManagedObject或其子类的实例。托管对象在托管对象上下文(NSManagedObjectContext)中注册。在任何给定的上下文中,托管对象最多有一个实例对应于持久存储中的给定记录。

CloudKit上,每条记录被称作为CKRecord

我们不需要关心Managed ObjectIDNSMangedObjectID)的创建过程,Core Data将为我们处理一切,但对于CKRecord,多数情况下,我们需要在代码中明确为每条记录设定CKRecordIdentifier。作为CKRecord的唯一标识,CKRecordIdentifier被用于确定该CKRecord在数据库的唯一位置。如果数据保存在自定义的CKRecordZone,我们也需要在CKRecord.ID中指明。

  • CKSubscription

CloudKit是云端服务,它要同一iCloud账户的不同设备(私有数据库)或者使用不同iCloud账号的设备(公共数据库)的数据变化做出相应的反馈。

开发者通过CloudKitiCloud上创建CKSubscription, 当CKContainer中的数据发生变化时,云端服务器会检查该变化是否满足某个CKSubscription的触发条件,在条件满足时,对订阅的设备发送远程提醒(Remote Notification)。这就是当我们在Xcode TargetSigning&Capabilities中添加上CloudKit功能时,会Xcode自动添加Remote Notification的原因。

在实际使用中,需要通过CKSubscription的三个子类完成不同的订阅任务:

CKQuerySubscription,当某个CKRecord满足设定的NSPercidate时推送Notification

CKDatabaseSubscription,订阅并跟踪数据库(CKDatabase)中记录的创建、修改和删除。该订阅只能用于私有数据库和共享数据库中自定义的CKRecordZone,并只会通知订阅的创建者。在以后的文章中,我们可以看到Core Data with CloudKit是如何在私有库中使用该订阅的。

CKRecordZoneNotification,当用户、或者在某些情况下,CloudKit修改该区域(CKRecordZone)的记录时,记录区的订阅就会执行,例如,当记录中某个字段的值发生变化时。

对于iCloud服务器推送的远程通知,应用程序需要在Application Delegate中做出响应。多数情况下,远程提醒可以采用静默通知的形式,为此开发者需要在的应用程序中启用Backgroud ModesRemote notifications

Core Data with CloudKit 的实现猜想

结合上面介绍的基础知识,让我们尝试推测一下Core Data with CloudKit的实现过程。

以私有数据库同步为例:

  • 初始化:
    1. 创建CKContainer
    2. 根据NSManagedObjectModel配置Schema
    3. 在私有数据库中创建 ID 为com.apple.coredata.cloudkit.zoneCKRecordZone
    4. 在私有数据库上创建CKDatabaseSubscription
  • 数据导出(将本地Core Data数据导出到云端)
    1. NSPersistentCloudKitContainer创建后台任务响应持久化历史跟踪NSPersistentStoreRemoteChange通知
    2. 根据NSPersistentStoreRemoteChangetransaction,将Core Data的操作转换成CloudKit的操作。比如对于新增数据,将NSManagedObject实例转换成CKRecord实例。
    3. 通过CloudKit将转换后的CKRecord或其他CloudKit 操作传递给iCloud服务器
  • 服务器端
    1. 按顺序处理从远端设备提交的CloudKit 操作数据
    2. 根据初始化创建的CKDatabaseSubscription检查该操作是否导致私有数据库的com.apple.coredata.cloudkit.zone中的数据发生变化
    3. 对所有创建CKDatabaseSubscription订阅的设备(同一iCloud账户)分发远程通知
  • 数据导入(将远程数据同步到本地)
    1. NSPersistentCloudKitContainer创建的后台任务响应云端的静默推送
    2. 向云端发送刷新操作要求并附上上次操作的令牌
    3. 云端根据每个设备的令牌,为其返回自上次刷新后数据库发生的变化
    4. 将远端数据转换成本地数据(删除、更新、添加等)
    5. 由于视图上下文automaticallyMergesChangesFromParen属性设置为真,本地数据的变化将自动在视图上下文中体现出来

上述步骤中省略了所有技术难点及细节,仅描述了大概的流程。

总结

本文中,我们简单介绍了关于Core DataCloudKit以及Core Data with CloudKit的一点基础知识。在下一篇文章中我们将探讨如何使用Core Data with CloudKit实现本地数据库和私有数据库的同步

PS:介绍如何使用 NSPersistentContainer 的文章并不少,但同其他 Core Data 的功能一样,用好并不容易。在两年多的使用中,我便碰到不少问题。借着今年打算在 【健康笔记 3】 中实现共享数据库功能的机会,我最近较系统地重新学习了Core Data with CloudKit并对其知识点进行了梳理。希望通过这个系列博文能让更多的开发者了解并使用Core Data with Cloudkit功能。

希望本文能够对你有所帮助。同时也欢迎你通过 TwitterDiscord 频道或下方的留言板与我进行交流。

本博客文章采用CC 4.0 协议,转载需注明出处和作者。

鼓励作者