tl;博士:
似乎在 7 天后清除持久历史记录几乎在所有情况下都有效。
如果必须同步 GB 的数据,它可能不会。
我做了什么:
我可以重现错误:
如果在 Apple 的演示应用程序中,在清除持久历史记录后同步数据,则可能会显示错误的数据。显然,一些对演示应用至关重要的信息已被删除。
下面,我开始使用干净的设置进行测试:
我从模拟器和设备中删除了该应用程序,并使用仪表板清除了 iCloud 私人数据库区域 com.apple.coredata.cloudkit.zone 中的所有 CD_Post 记录。
为了检查可能被无意删除的信息,我在 func processPersistentHistory() 中插入了一个 print 语句,用于过滤交易的持久历史记录:
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 的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>)
这向我表明设备中的持久存储有正确的数据,只有显示的表格是错误的。
接下来我调查了MainViewController中的func update。这个函数是从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: []
这向我表明:
-
NSManagedObjectContext.mergeChanges 之前的表格是正确的,即它包含帖子 p1 和 p2。
- 再次合并两个帖子。
-
viewContext 中的两个帖子均已删除 (AnyHashable("deleted"))。
-
fetchedResultsController 回复同时删除了这两个帖子 (NSFetchedResultsChangeType(rawValue: 2))。
- 最终记录到
fetchedResultsController 没有对象,因此表为空。
作为最后的检查,我在func processPersistentHistory() 中注释掉了清除历史记录的代码,并且正如预期的那样,表格显示正确,当我在模拟器中输入另一个条目时也是如此。
结论是什么?
- 在持久存储(模拟器和设备)以及 iCloud 中,所有数据始终正确。
- 如果镜像软件没有足够的时间来处理其在持久历史记录中的条目,则将远程存储更改合并到上下文会失败。
- 这需要多长时间可能取决于必须同步的数据量。我的经验是一些 kb 需要几秒钟,但这当然取决于许多参数。但如果是这样,7 天对应于一些 GB 同步,这是相当不寻常的。在这方面,在 7 天后清除持久性历史记录似乎是内存消耗和正确应用运行之间的良好折衷。
重现测试的更多提示(这可能对尝试相同的其他人有所帮助):
按照建议,我下载了 Apple 的演示应用程序和您修改的核心数据堆栈。
它确实为模拟器编译,但对于设备,我必须在目标的 Signing & Capabilities 选项卡中设置 3 个附加设置:
- 设置开发团队
- 将包标识符设置为合理的值,例如
com.<your company>.CoreDataCloudKitDemo。
- 选择正确的 iCloud 容器,例如
iCloud.com.<your company>.CoreDataCloudKitDemo。
- 此外,我必须确保模拟器和设备登录到同一个 iCloud 帐户。请注意,对于模拟器,必须每天重新登录一次。大多数情况下会提醒您这样做,但有时不会。
然后,我可以在模拟器和设备上运行该应用程序。
我在 CloudKit 控制台中验证,在私有数据库区域 com.apple.coredata.cloudkit.zone 中没有 CD_Post 类型的记录。由于不共享数据,因此不使用 iCloud 共享数据库。