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的用法、调试技巧、控制台设置并尝试更深入地研究其同步机制。

健康笔记是我开发的一个iOS app,主要服务于有长期健康管理需求的人士。健康笔记提供了强大的自定义数据类型功能,可以满足记录生活中绝大多数的健康项目数据的需要。你可以为每个家庭成员创建各自的健康数据记录笔记,或者针对某个特定项目、特定时期创建对应的笔记。

推广

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功能。

本博客所有文章除特别声明外,均采用CC 4.0许可协议。转载请注明出处和作者。

关注微信公共号肘子的Swift记事本或在推特上关注@fatbobman,永远不会错过新内容! 您的支持和鼓励将为我的博客写作增添更多的动力! 如果您或身边的朋友有健康数据管理的需求,请使用我开发的app【健康笔记】,正是因为它我才创建了这个博客。

关注