如何将 newBackgroundContext() 与 URLSession.shared.dataTaskPublisher 一起使用?

How to use newBackgroundContext() with URLSession.shared.dataTaskPublisher?

我创建了 a simple Core Data project at Github 来演示我的问题:

我的测试应用程序下载了一个 JSON 对象列表,将其存储在 Core Data 中并通过 @FetchRequest.

显示在 SwiftUI 列表中

因为对象列表在我的 real 应用程序中有 1000 多个元素,我想在后台线程而不是主线程上将实体保存到核心数据中.

我最好使用相同的默认后台线程,URLSession.shared.dataTaskPublisher.

已经使用了该线程

所以在 Xcode 生成的标准 Persistence.swift 中我只添加了 2 行:

container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
container.viewContext.automaticallyMergesChangesFromParent = true

在我的 DownloadManager.swift 单例中,我调用:

static let instance = DownloadManager()

var cancellables = Set<AnyCancellable>()

// How to run this line on the background thread of URLSession.shared.dataTaskPublisher?
let backgroundContext = PersistenceController.shared.container.newBackgroundContext()

private init() {
    getTops()
}

func getTops() {
    guard let url = URL(string: "https://slova.de/ws/top") else { return }
    
    URLSession.shared.dataTaskPublisher(for: url)
        .tryMap(handleOutput)
        .decode(type: TopResponse.self, decoder: JSONDecoder())
        .sink { completion in
            print(completion)
        } receiveValue: { [weak self] returnedTops in
            for top in returnedTops.data {
                // the next line fails with EXC_BAD_INSTRUCTION
                let topEntity = TopEntity(context: self!.backgroundContext)
                topEntity.uid = Int32(top.id)
                topEntity.elo = Int32(top.elo)
                topEntity.given = top.given
                topEntity.avg_score = top.avg_score ?? 0.0
            }
            self?.save()
        }
        .store(in: &cancellables)
}

如您在上面的屏幕截图中所见,此操作失败

Thread 4: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

因为我在 Xcode 中添加了以下“启动时传递的参数”:

-com.apple.CoreData.ConcurrencyDebug 1

谁能告诉我如何在正确的线程上调用 newBackgroundContext()

更新:

我已尝试按照以下代码解决我的问题,但错误是相同的:

URLSession.shared.dataTaskPublisher(for: url)
    .tryMap(handleOutput)
    .decode(type: TopResponse.self, decoder: JSONDecoder())
    .sink { completion in
        print(completion)
    } receiveValue: { returnedTops in
        let backgroundContext = PersistenceController.shared.container.newBackgroundContext()

        for top in returnedTops.data {
            // the next line fails with EXC_BAD_INSTRUCTION
            let topEntity = TopEntity(context: backgroundContext)
            topEntity.uid = Int32(top.id)
            topEntity.elo = Int32(top.elo)
            topEntity.given = top.given
            topEntity.avg_score = top.avg_score ?? 0.0
        }

        do {
            try backgroundContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }

更新 2:

我假设当调用 newBackgroundContext() 时,它使用当前线程,然后您可以从同一线程使用该上下文...

这似乎不是这种情况,我必须调用 performperformAndWaitperformBackgroundTask(我已更新 my code at Github 来做到这一点)。

我仍然想知道,newBackgroundContext 的线程是否可以与 URLSession.shared.dataTaskPublisher 的线程相同...

看来你已经自己解决了大部分问题。

This seems not to be the case and I have to call perform, performAndWait or performBackgroundTask (I have updated my code at Github to do just that).

还没解决的就在这里-

Still I wonder, if the thread of newBackgroundContext can be the same as of the URLSession.shared.dataTaskPublisher...


URLSession API 允许您提供自定义响应队列,如下所示。

URLSession.shared
    .dataTaskPublisher(for: URL(string: "https://google.com")!)
    .receive(on: DispatchQueue(label: "API.responseQueue", qos: .utility)) // <--- HERE
    .sink(receiveCompletion: {_ in }, receiveValue: { (output) in
          print(output)
     })

或者传统上是这样的-

let queue = OperationQueue()
queue.underlyingQueue = DispatchQueue(label: "API.responseQueue", qos: .utility)
let session = URLSession(configuration: .default, delegate: self, delegateQueue: queue)

所以理想情况下,我们应该传入 NSManagedObjectContextDispatchQueue 作为 URLSession 的响应队列。


问题在于 NSManagedObjectContext API -

  1. 两者都不允许您从外部提供自定义 DispatchQueue 实例。
  2. 也不会公开只读 属性 因为它是内部管理的 DispatchQueue 实例。

我们无法访问 NSManagedObjectContext 实例的基础 DispatchQueue。此规则的唯一例外是使用 DispatchQueue.main.viewContext。当然,我们不想处理网络响应解码和数千条记录 created/persisted on/from 主线程。