PAYATHON 2026

NSPersistentCloudKitContainer.share() 卡死与共享权限不持久化如何排查?

数据阿明

仅凭这些代码,无法确定卡死的具体原因,也无法认定是 iOS 26 的框架缺陷。后台队列不一定是 Core Data context 所属的队列,修改 CKShare.publicPermission 也不代表已经保存到服务端。仅凭两项内部 metadata 的值,无法证明存储损坏。

为什么换到后台队列仍然可能卡死?

第一段代码在 bgContext.performAndWait 中取得 NSManagedObject,随后把对象带到 global queue 上调用 share()。第二段代码也让后台 context 的对象离开了 perform 的执行范围。这两种写法都需要先解决对象跨队列使用的问题。

Core Data 要求在对象所属 context 的队列上使用对象。跨队列传递时,应传递 NSManagedObjectID,再由目标 context 获取对象。DispatchQueue.global、Task.detached 和 nonisolated 都不会改变这一约束。参见 Apple 的后台 Core Data 使用说明。

这些并发问题需要修正,但尚不能确定它们是否是此次死锁的根因。

可以先建立一个更简单的诊断基线:用主队列的 viewContext 获取已保存的对象,在同一隔离范围内发起 callback 版本的共享调用,并立即返回事件循环。

import CoreData
import CloudKit

@MainActor
func probeShare(
    container: NSPersistentCloudKitContainer,
    objectID: NSManagedObjectID
) throws {
    precondition(!objectID.isTemporaryID)

    let context = container.viewContext
    let object = try context.existingObject(with: objectID)

    container.share([object], to: nil) { _, share, _, error in
        if let error {
            print("share failed:", error)
            return
        }
        print("share completed:", share?.recordID as Any)
    }
}

调用前应保存对象所在的 context;如果使用父子 context,还需要完成保存到 persistent store 的过程。这个示例用于减少并发变量,把调用放到主线程并不一定能修复卡死。

同时检查调用链中是否存在 DispatchSemaphore.wait()、DispatchGroup.wait()、DispatchQueue.main.sync,或在 context、通知处理器中同步等待异步共享完成的代码。后台任务如果持有其他线程需要的锁,仍可能让 UI 停住。普通的异步等待本身不足以解释主线程完全冻结。

如何让 publicPermission 真正保存?

publicPermission 默认是 .none。将它设置为 .readWrite 并保存后,持有链接的用户才能按公开共享权限加入。公开链接共享仍属于 CloudKit sharing,不需要改用 public database。参见 publicPermission 文档。

对于 Core Data 管理的 share,程序修改权限后,应通过 persistUpdatedShare(_:in:completion:) 保存,以保持共享记录和本地 metadata 一致:

// share 是共享创建成功后返回的 CKShare。
// ownerStore 是该共享所属的拥有者 private persistent store。
share.publicPermission = .readWrite

persistentContainer.persistUpdatedShare(share, in: ownerStore) {
    savedShare, error in

    if let error {
        print("persistUpdatedShare failed:", error)
        return
    }

    print("share exported:", savedShare?.url as Any)
}

容器同时配置了 private store 和 shared store 时,不要把拥有者的 share 保存到接收者使用的 shared store,也不要默认 persistentStores.first 一定对应正确的 store。

这个 API 会在方法返回前把 share 保存到本地 persistent store,completion 则要等导出完成后才调用。因此,即使用 fire-and-forget 的方式不等待 completion,调用仍可能卡在方法内部的本地保存阶段。分别记录“进入方法”“方法返回”和“completion 执行”,才能确定停在哪里。参见 persistUpdatedShare 文档。

prepareShare 中应该如何安排保存流程?

prepareShare 的约定是:创建共享,将共享及相关记录保存到服务端,然后返回保存后的 CKShare。仅执行属性赋值后 return share,不能作为权限已经上传成功的依据。参见 prepareShare 文档。

如果由程序设置权限,流程应当是:

  1. 创建共享,或取得对象已有的共享。
  2. 设置 publicPermission = .readWrite。
  3. 调用 persistUpdatedShare,异步等待成功或传播错误。
  4. 返回保存成功后的 share。

不要用 semaphore 把这个异步流程改成同步等待。当前保存操作已经会卡住,需要先定位阻塞位置;再套一层 Task 或 continuation 不会解除底层锁等待。

如果共享及权限可以提前准备好,可以通过 .existing 交给系统共享界面。对于“任何持有链接的人均可读写”的目标,还可以明确限制系统界面允许的选项:

let options = CKAllowedSharingOptions(
    allowedParticipantPermissionOptions: .readWrite,
    allowedParticipantAccessOptions: .anyoneWithLink
)

return .existing(
    savedShare,
    container: ckContainer,
    allowedSharingOptions: options
)

这里的 savedShare 必须是已经成功保存的共享。CKAllowedSharingOptions 控制系统共享界面允许用户选择的范围,本身不是持久化操作,也不能代替 persistUpdatedShare。参见 CKAllowedSharingOptions 文档。

操作只在 prepareShare 内成功,说明两条调用路径的执行条件不同;但这不能证明 share() 只能在该 closure 中使用,也不能证明系统共享界面会自动上传任意属性修改。

如何区分死锁和 CloudKit 导出等待?

UI 冻结时,暂停 Xcode 调试器并查看所有线程,重点检查主线程、context 队列和 persistent store coordinator 相关线程。只有找到相互等待的调用栈,才能确认死锁及其参与者。

如果 UI 仍可操作,只是 completion 长时间不执行,还需要检查 CloudKit 导出进度、错误和系统调度。NSPersistentCloudKitContainer 的同步会受到网络条件、系统调度和节流影响,回调未完成不能单独作为死锁证据。参见 TN3163 同步机制说明。

诊断时可以启用 Core Data 并发检查启动参数:

-com.apple.CoreData.ConcurrencyDebug 1

同时记录 NSPersistentCloudKitContainer.eventChangedNotification 中的事件及错误。原示例中的 try? 和 obj! 也应改为明确处理错误,避免掩盖对象获取失败的信息。

遗留 zone metadata 是否可能导致卡死?

可以把它作为调查线索,但 needsShareUpdate = 1 与 encodedShareAsset = nil 并不是足以独立判断损坏的公开状态约定。不能据此得出“孤立 metadata 导致死锁”的结论,也不要手工修改 Core Data 内部表。

应在可丢弃的测试环境中,用新的对象和独立测试数据重复相同步骤,比较旧数据与新数据的调用栈、同步事件和错误。如果只有旧数据复现,再进一步调查删除共享后的状态;如果新数据也复现,应继续检查调用链和框架执行路径。

现有的 CKSharingSupported、Production schema 和 TestFlight 配置检查有帮助,但不能代替对具体 share 的核验。应确认链接对应当前共享,并在拥有者的正确 container、Production 环境及 private database 中重新读取该 CKShare,核实服务端 publicPermission。如果权限已是 .readWrite,接收者仍提示 Object not available,就需要继续检查共享记录是否存在、链接是否过期,以及接受共享时返回的具体错误。

备注:内容仅供参考。