CoreData + CloudKit网络故障后数据不一致

CoreData + CloudKit Data Inconsistency after Network Failure

我的应用程序使用 CoreData + CloudKit 镜像来同步数据,例如在 iPhone 和手表上。
如果在一台设备上修改了数据,则将修改内容上传到 iCloud 并稍后与其他设备同步。
这通常工作正常。但是很少会发生以下情况:

设备上的数据被修改,应用程序被终止。
下次重新启动应用程序时,显示的不是修改后的数据,而是未修改的版本。
我假设(我不知道 CoreData + CloudKit 镜像在内部是如何工作的)以下问题。

问题:

考虑以下设置:有一个 CoreData 实体 Item,其中包含一些属性 updatedAt: Date?。 每次更改属性时,都会更新 updatedAt,并将 Item 保存到镜像到 iCloud 的持久存储中。 保存后,更新后的Item导出到iCloud。
当应用程序终止并稍后重新启动时,将导入 iCloud 版本,这不会产生任何影响,因为它是修改后的版本。 然而:
如果应用程序在可以上传修改版本之前终止,例如因为没有网络连接,iCloud上还是未修改的版本。
重新启动应用程序后,将导入具有较旧 updatedAt 值的未修改版本,并使用较新的 updatedAt 值覆盖修改后的版本。 所以修改丢失。

可能的解决方案?:

我的第一个想法是使用两个持久存储,一个未镜像的 localStore 和一个镜像的 mirrorStore。 实体 Item 已分配给这两家商店。保存 Item 时,它会保存到两个存储区。
通常,即没有上述问题,两家商店都有相同的 Item 副本。 当提取 Item 时,通过相应地设置提取请求的 affectedStores 属性 仅从 localStore 提取它。

但是,当问题出现时,mirrorStore中的Item被旧版本覆盖了。 这可以通过监听 mirrorStore.NSPersistentStoreRemoteChange 通知来处理。 收到通知后,可以从 localStoremirrorStore 中获取 Item,并且 select 具有更新的 updatedAt 值的版本。 在所描述的场景中,这始终是 localStore 中的 Item,但如果稍后在另一台设备上修改了 Item,则 mirrorStore 中的版本也可能更新。在任何情况下,旧版本都必须被新版本覆盖。 这可以通过删除旧版本并将新版本再次保存到两个商店来完成。然后数据再次一致。

我的问题:

编辑:

我现在意识到应用程序意外终止的原因之一。
后台 CoreData+CloudKit 导出在 Watch 上可能需要很长时间,请参见以下日志:

2022-03-31 11:18:12.910276+0200 Watch Extension[2388:703470] [BackgroundTask]  
Background Task 122 ("CoreData: CloudKit Export"), was created over 30 seconds  
ago. In applications running in the background, this creates a risk of termination.  
Remember to call UIApplication.endBackgroundTask(_:) for your task in a timely  
manner to avoid this.  
…  
2022-03-31 11:19:00.036156+0200 Watch Extension[2388:703470] [BackgroundTask]  
Background task still not ended after expiration handlers were called:  
<_UIBackgroundTaskInfo: 0x16514b00>: taskID = 122, taskName = CoreData:  
CloudKit Export, creationTime = 61315 (elapsed = 82).  
This app will likely be terminated by the system.  
Call UIApplication.endBackgroundTask(_:) to avoid this.

问题:

描述的问题确实存在。
基本原因是 CoreData & CloudKit 无法确定 CoreData 记录(CoreData 托管对象)或其镜像 iCloud 记录(CKRecord)是否为“真实来源”,即有效记录,以防它们不同。
在我能想象的所有应用程序中,最后修改的记录是有效记录(错误除外)。
现在,CKRecords 有一个系统 属性 modificationDate 会在 CKRecord 更改时自动更新。但是,CoreData 实体没有像 modificationDate 这样的系统属性。因此,CoreData & CloudKit 无法 select 将后来修改的记录作为真实来源。
如果应用程序启动或进入前台,CoreData & CloudKit 首先触发从 iCloud 导入并更新持久存储。这意味着尚未导出到 iCloud 的本地更新,例如由于网络问题,应用程序在导出之前已终止,将被覆盖并丢失。

我的解决方案:

我所有的 CoreData 实体都有一个属性 modificationDate。这样的属性也可以自动设置,参见 here。 我的 CoreData 记录存储在本地持久存储中,而 iCloud 私有数据库由 CoreData & CloudKit 镜像到另一个私有持久存储。当这个私有持久存储被镜像更新时,它会发送一个 .NSPersistentStoreRemoteChange 通知。处理通知的函数比较 CoreData 和 iCloud 记录的 modificationDate 字段,selects 是较新的,即更新本地持久存储或私有持久存储中的记录。
当然,托管上下文和持久存储之间可能存在冲突。这种冲突也必须通过 selecting 更新版本的 CoreData 记录来解决。但是,这可以通过使用自定义合并策略来处理,如 here.

所述