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
通知来处理。
收到通知后,可以从 localStore
和 mirrorStore
中获取 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.
所述
我的应用程序使用 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
通知来处理。
收到通知后,可以从 localStore
和 mirrorStore
中获取 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.