UPDATE: 对于 Core Data + CloudKit 设置尤其重要
In 这个帖子 https://useyourloaf.com/blog/wwdc22-core-data-lab-notes/来自 WWDC22 核心数据实验室的 Apple Core Data 框架工程师回答了这个问题“我是否需要清除持久历史跟踪数据?“ 如下:
不,我们不推荐。 NSPersistentCloudKitContainer 使用
用于跟踪同步内容的持久历史记录令牌。如果删除历史记录
云同步已重置,必须从头开始上传所有内容。它
会恢复,但这不是良好的客户体验。不应该
通常需要删除历史记录。例如,苹果照片
应用程序不会修剪其历史记录,因此除非您生成大量
大量的历史并不能做到这一点。
tl;dr:
似乎在 7 天后清除持久历史记录几乎在所有情况下都有效。
如果必须同步 GB 级的数据,则可能不会。
我做了什么:
我可以重现该错误:
如果在Apple的演示应用程序中,在清除持久历史记录后同步数据,则可能会显示错误的数据。显然,一些对于演示应用程序至关重要的信息已被删除。
下面,我开始使用干净的设置进行测试:
我从模拟器和设备中删除了该应用程序,并清除了所有CD_Post
iCloud 私有数据库、区域中的记录com.apple.coredata.cloudkit.zone
,使用仪表板。
为了检查可能被无意删除的信息,我插入了func processPersistentHistory()
Guard 语句中的打印语句用于过滤事务的持久历史记录:
guard let transactions = result?.result as? [NSPersistentHistoryTransaction],
!transactions.isEmpty
else {
print("**************** \(String(describing: result?.result))")
return
}
如果我在 Xcode 下的模拟器上运行该应用程序,则不会按预期显示任何条目,并且日志现在显示许多此类条目:
**************** Optional(<__NSArray0 0x105a61900>(
)
)
显然,持久历史记录包含 iCloud 镜像内务信息,当持久历史记录被清除时,这些信息也会被删除。这向我表明镜像软件需要“足够的时间”才能成功完成其操作,因此只应清除“旧”历史条目。但什么是“老”呢? 7天?
接下来,在 Xcode 下的模拟器上,我安装并执行了应用程序并立即清除,如问题的测试 1 中所示。
// Remove history before the last history token
let purgeHistoryRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: lastHistoryToken)
do {
try taskContext.execute(purgeHistoryRequest)
} catch {
print("\(error)")
}
在模拟器上,我添加了一个条目。该条目显示在仪表板中。
然后,在 Xcode 下的设备上,我还安装并执行了该应用程序并立即清除。该条目已正确显示,即 iCloud 记录已镜像到设备的持久存储,历史记录已处理并立即清除,尽管镜像软件可能没有“足够的时间”来成功完成其操作。
在模拟器上,我添加了第二个条目。该条目也显示在仪表板中。
然而,设备上的第一个条目消失了,即表格现在为空,但两个条目仍显示在仪表板中,即iCloud 数据未损坏.
然后我设置一个断点DispatchQueue.main.async
of func processPersistentHistory()
。仅当处理持久存储的远程更改时才会到达此断点。为了到达设备中的断点,我在模拟器中添加了第三个条目。因此,在设备中到达了断点,并且在调试器中我输入了
(lldb) po taskContext.fetch(Post.fetchRequest())
▿ 3 elements
- 0 : <Post: 0x281400910> (entity: Post; id: 0xbc533cc5eb8b892a <x-coredata://C9DEC274-B479-4AF5-9349-76C1BABB5016/Post/p3>; data: <fault>)
- 1 : <Post: 0x281403d90> (entity: Post; id: 0xbc533cc5eb6b892a <x-coredata://C9DEC274-B479-4AF5-9349-76C1BABB5016/Post/p4>; data: <fault>)
- 2 : <Post: 0x281403390> (entity: Post; id: 0xbc533cc5eb4b892a <x-coredata://C9DEC274-B479-4AF5-9349-76C1BABB5016/Post/p5>; data: <fault>)
这向我表明设备中的持久化存储有正确的数据,只有显示的表是错误的.
接下来我调查了func update
in the MainViewController
。这个函数是从调用的func didFindRelevantTransactions
,在处理历史记录并过帐相关交易时调用。在我的测试期间,transactions.count
总是 transactions.forEach.
我试图找出什么NSManagedObjectContext.mergeChanges
做。因此我将代码修改为
transactions.forEach { transaction in
guard let userInfo = transaction.objectIDNotification().userInfo else { return }
let viewContext = dataProvider.persistentContainer.viewContext
print("BEFORE: \(dataProvider.fetchedResultsController.fetchedObjects!)")
print("================ mergeChanges: userInfo: \(userInfo)")
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: userInfo, into: [viewContext])
print("AFTER: \(dataProvider.fetchedResultsController.fetchedObjects!)")
}
看看会发生什么viewContext
,我实现了
@objc func managedObjectContextObjectsDidChange(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }
print(#function, userInfo)
}
并看看这如何影响fetchedResultsController
,我也实现了
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange anObject: Any,
at indexPath: IndexPath?,
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?) {
print("**************** ", #function, "\(type) ", anObject)
}
为了使日志相对较短,我在仪表板中删除了所有CD_Post
除第一个条目外的条目,并从模拟器和设备中删除该应用程序。
然后,我在 Xcode 下运行模拟器和设备上的应用程序。两者都显示第一个条目。
然后我在模拟器中输入了另一个条目。不幸的是,正如预期的那样,设备上的表被清除了。这是设备的日志:
BEFORE: [<Post: 0x2802c2d50> (entity: Post; id: 0x9aac7c6d193c7772 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/Post/p1>; data: {
attachments = (
);
content = nil;
location = nil;
tags = (
);
title = "Untitled 3:40:24 PM";
}), <Post: 0x2802d2a80> (entity: Post; id: 0x9aac7c6d195c7772 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/Post/p2>; data: <fault>)]
================ mergeChanges: userInfo: [AnyHashable("deleted_objectIDs"): {(
0x9aac7c6d195c7772 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/Post/p2>,
0x9aac7c6d193c7772 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/Post/p1>
)}]
managedObjectContextObjectsDidChange(notification:) [AnyHashable("managedObjectContext"): <_PFWeakReference: 0x2821a8100>, AnyHashable("deleted"): {(
<Post: 0x2802d2a80> (entity: Post; id: 0x9aac7c6d195c7772 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/Post/p2>; data: {
attachments = (
);
content = nil;
location = nil;
tags = (
);
title = nil;
}),
<Post: 0x2802c2d50> (entity: Post; id: 0x9aac7c6d193c7772 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/Post/p1>; data: {
attachments = (
);
content = nil;
location = nil;
tags = (
);
title = "Untitled 3:40:24 PM";
})
)}, AnyHashable("NSObjectsChangedByMergeChangesKey"): {(
)}]
**************** controller(_:didChange:at:for:newIndexPath:) NSFetchedResultsChangeType(rawValue: 2) <Post: 0x2802d2a80> (entity: Post; id: 0x9aac7c6d195c7772 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/Post/p2>; data: {
attachments = (
);
content = nil;
location = nil;
tags = (
);
title = nil;
})
**************** controller(_:didChange:at:for:newIndexPath:) NSFetchedResultsChangeType(rawValue: 2) <Post: 0x2802c2d50> (entity: Post; id: 0x9aac7c6d193c7772 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/Post/p1>; data: {
attachments = (
);
content = nil;
location = nil;
tags = (
);
title = "Untitled 3:40:24 PM";
})
managedObjectContextObjectsDidChange(notification:) [AnyHashable("updated"): {(
<NSCKRecordZoneMetadata: 0x2802ce9e0> (entity: NSCKRecordZoneMetadata; id: 0x9aac7c6d193c77d2 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/NSCKRecordZoneMetadata/p1>; data: {
ckOwnerName = "__defaultOwner__";
ckRecordZoneName = "com.apple.coredata.cloudkit.zone";
currentChangeToken = "<CKServerChangeToken: 0x2823fcdc0; data=AQAAAAAAAACQf/////////+gT9nZvOBLv7hsIaI3NVdg>";
database = "0x9aac7c6d193c77e2 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/NSCKDatabaseMetadata/p1>";
encodedShareData = nil;
hasRecordZoneNum = 1;
hasSubscriptionNum = 0;
lastFetchDate = "2022-06-15 13:55:25 +0000";
mirroredRelationships = "<relationship fault: 0x2821a3c60 'mirroredRelationships'>";
needsImport = 0;
needsRecoveryFromIdentityLoss = 0;
needsRecoveryFromUserPurge = 0;
needsRecoveryFromZoneDelete = 0;
needsShareDelete = 0;
needsShareUpdate = 0;
queries = "<relationship fault: 0x2821a2560 'queries'>";
records = (
);
supportsAtomicChanges = 1;
supportsFetchChanges = 1;
supportsRecordSharing = 1;
supportsZoneSharing = 1;
})
)}, AnyHashable("managedObjectContext"): <_PFWeakReference: 0x2821a1900>, AnyHashable("deleted"): {(
<NSCKRecordMetadata: 0x2802ce850> (entity: NSCKRecordMetadata; id: 0x9aac7c6d193c7762 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/NSCKRecordMetadata/p1>; data: {
ckRecordName = "3FB952E5-6B30-472E-BC6E-0116FA507B88";
ckRecordSystemFields = nil;
ckShare = nil;
encodedRecord = "{length = 50, bytes = 0x6276786e f7090000 52070000 e0116270 ... 61726368 69000ee0 }";
entityId = 3;
entityPK = 1;
lastExportedTransactionNumber = nil;
moveReceipts = (
);
needsCloudDelete = 0;
needsLocalDelete = 0;
needsUpload = 0;
pendingExportChangeTypeNumber = nil;
pendingExportTransactionNumber = nil;
recordZone = nil;
}),
<NSCKRecordMetadata: 0x2802cdcc0> (entity: NSCKRecordMetadata; id: 0x9aac7c6d195c7762 <x-coredata://496D2B54-DDB9-47EF-945A-CC1DBA1E14E8/NSCKRecordMetadata/p2>; data: {
ckRecordName = "0919480D-16CB-49F9-8351-9471371040AC";
ckRecordSystemFields = nil;
ckShare = nil;
encodedRecord = "{length = 50, bytes = 0x6276786e f7090000 52070000 e0116270 ... 61726368 69000ee0 }";
entityId = 3;
entityPK = 2;
lastExportedTransactionNumber = nil;
moveReceipts = (
);
needsCloudDelete = 0;
needsLocalDelete = 0;
needsUpload = 0;
pendingExportChangeTypeNumber = nil;
pendingExportTransactionNumber = nil;
recordZone = nil;
})
)}]
managedObjectContextObjectsDidChange(notification:) [AnyHashable("managedObjectContext"): <_PFWeakReference: 0x2821a3060>, AnyHashable("invalidatedAll"): <__NSArrayM 0x282f75830>(
)
]
AFTER: []
这向我表明:
- Before
NSManagedObjectContext.mergeChanges
,该表是正确的,即它包含帖子 p1 和 p2。
- 两个帖子再次合并。
- In the
viewContext
,两个帖子都被删除了(AnyHashable("deleted")
).
- The
fetchedResultsController
回应也删除了这两个帖子(NSFetchedResultsChangeType(rawValue: 2)
).
- 最终记录的是
fetchedResultsController
没有对象,因此表是空的。
作为最后检查,我发表了评论func processPersistentHistory()
清除历史记录的代码,正如预期的那样,当我在模拟器中输入另一个条目时,该表也正确显示。
结论是什么?
- 在持久存储(模拟器和设备)以及 iCloud 中,所有数据始终正确。
- 如果镜像软件没有足够的时间来处理持久历史记录中的条目,则将远程存储更改合并到上下文会失败。
- 这需要多长时间可能取决于必须同步的数据量。我的经验是,某些 kb 需要几秒钟,但这当然取决于许多参数。但如果是这样,7 天相当于需要同步一些 GB,这是相当不寻常的。在这方面,7 天后清除持久历史记录似乎是内存消耗和正确的应用程序操作之间的一个很好的折衷方案。
重现测试的进一步提示(这可能会帮助其他尝试相同的人):
按照建议,我下载了Apple的演示应用程序和您修改的核心数据堆栈。
它确实针对模拟器进行了编译,但对于设备,我必须在目标的“签名和功能”选项卡中设置 3 个附加设置:
- 设置开发团队
- 将包标识符设置为合理的值,例如
com.<your company>.CoreDataCloudKitDemo
.
- 选择正确的 iCloud 容器,例如
iCloud.com.<your company>.CoreDataCloudKitDemo
.
- 此外,我必须确保模拟器和设备登录到同一个 iCloud 帐户。请注意,对于模拟器来说,大约每天必须重新登录一次。大多数情况下,人们都会被提醒这样做,但有时却不会。
然后,我可以在模拟器和设备上运行该应用程序。
我在 CloudKit 控制台中验证了私有数据库区域com.apple.coredata.cloudkit.zone
没有 CD_Post 类型的记录。由于数据不共享,因此不使用 iCloud 共享数据库。