0

在 Github 创建了一个简单的 Core Data 项目来演示我的问题:

Xcode 截图

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

因为对象列表在我的真实应用程序中有 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)
}

正如您在上面的屏幕截图中看到的那样,这失败了

线程 4:EXC_BAD_INSTRUCTION(代码=EXC_I386_INVOP,子代码=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()被调用时,它会占用当前线程,然后您可以从同一个线程使用该上下文......

情况似乎并非如此,我必须调用perform,performAndWait或(我已经在 GithubperformBackgroundTask上更新了我的代码来做到这一点)。

仍然我想知道,如果线程newBackgroundContext可以与URLSession.shared.dataTaskPublisher...

4

1 回答 1

1

似乎您自己已经解决了大部分问题。

情况似乎并非如此,我必须调用 perform,performAndWait或者performBackgroundTask(我已经在 Github 上更新了我的代码来做到这一点)。

还没解决的就在这里——

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


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

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)

所以理想情况下,我们应该将NSManagedObjectContext'sDispatchQueue作为URLSession.


问题在于NSManagedObjectContextAPI -

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

DispatchQueue例如,我们无法访问底层NSManagedObjectContext。此规则的唯一例外是.viewContext使用DispatchQueue.main. 当然,我们不想处理网络响应解码以及在主线程上/从主线程创建/保留的数千条记录。

于 2021-06-18T20:22:40.410 回答