为什么在使用 CoreData、NSFetchedResultsController 和 Diffable Data Source 时需要 DispatchQueue.main.async 讨论你需要做什么?

问题描述

在处理 CoreData、NSFetchedResultsController 和 Diffable Data Source 时,我总是注意到我需要应用 dispatchQueue.main.async

例如

应用 dispatchQueue.main.async 之前

extension ViewController: NSFetchedResultsControllerDelegate {
    func controller(_ fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>,didChangeContentWith snapshotReference: NSDiffableDataSourceSnapshotReference) {
        guard let dataSource = self.dataSource else {
            return
        }
        
        var snapshot = snapshotReference as NSDiffableDataSourceSnapshot<String,NSManagedobjectID>

        dataSource.apply(snapshot,animatingDifferences: true) { [weak self] in
            guard let self = self else { return }
        }
    }
}

但是,我们在 performFetch 中运行 viewDidLoad 后,在 dataSource.apply 中会出现以下错误

'检测到死锁:在主队列上调用方法 不允许未完成的异步更新,并且会死锁。请 总是提交更新,要么总是在主队列上,要么总是关闭 主队列

我可以使用以下方法解决”问题

应用 dispatchQueue.main.async 后

extension ViewController: NSFetchedResultsControllerDelegate {
    func controller(_ fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>,didChangeContentWith snapshotReference: NSDiffableDataSourceSnapshotReference) {
        dispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            
            guard let dataSource = self.dataSource else {
                return
            }
            
            var snapshot = snapshotReference as NSDiffableDataSourceSnapshot<String,NSManagedobjectID>

            dataSource.apply(snapshot,animatingDifferences: true) { [weak self] in
                guard let self = self else { return }
            }
        }
    }
}

之后一切正常。

但是,我们对为什么需要 dispatchQueue.main.async 感到困惑,因为

  1. performFetch 在主线程中运行。
  2. 回调 didChangeContentWith 在主线程中运行。
  3. NSFetchedResultsController 使用主 CoreData 上下文,而不是背景上下文。

因此,如果不使用 dispatchQueue.main.async,我们无法理解为什么会出现运行时错误

你知道为什么在使用 CoreData、NSFetchedResultsController 和 Diffable Data Source 时需要 dispatchQueue.main.async 吗?

以下是我们的详细代码片段。

CoreDataStack.swift

import CoreData

class CoreDataStack {
    public static let INSTANCE = CoreDataStack()
    
    private init() {
    }
    
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "xxx")
        
        container.loadPersistentStores(completionHandler: { (storeDescription,error) in
            if let error = error as NSError? {
                // This is a serIoUs Fatal error. We will just simply terminate the app,rather than using error_log.
                fatalError("Unresolved error \(error),\(error.userInfo)")
            }
        })
        
        // So that when backgroundContext write to persistent store,container.viewContext will retrieve update from
        // persistent store.
        container.viewContext.automaticallyMergesChangesFromParent = true
        
        // Todo: Not sure these are required...
        //
        //container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        //container.viewContext.undoManager = nil
        //container.viewContext.shouldDeleteInaccessibleFaults = true
        
        return container
    }()
    
    lazy var backgroundContext: NSManagedobjectContext = {
        let backgroundContext = persistentContainer.newBackgroundContext()

        // Todo: Not sure these are required...
        //
        backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        //backgroundContext.undoManager = nil
        
        return backgroundContext
    }()
    
    // https://www.avanderlee.com/swift/nsbatchdeleterequest-core-data/
    func mergeChanges(_ changes: [AnyHashable : Any]) {
        
        // Todo:
        //
        // (1) Should this method called from persistentContainer.viewContext,or backgroundContext?
        // (2) Should we include backgroundContext in the into: array?
        
        NSManagedobjectContext.mergeChanges(
            fromremoteContextSave: changes,into: [persistentContainer.viewContext,backgroundContext]
        )
    }
}

NoteViewController.swift

class NoteViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        ...
        initDataSource()
        initNSTabInfoProvider()
    }

    
    private func initNSTabInfoProvider() {
        self.nsTabInfoProvider = NSTabInfoProvider(self)
        
        // Trigger performFetch
        _ = self.nsTabInfoProvider.fetchedResultsController
    }

    private func initDataSource() {
        let dataSource = DataSource(
            collectionView: tabCollectionView,cellProvider: { [weak self] (collectionView,indexPath,objectID) -> UICollectionViewCell? in
                
                guard let self = self else { return nil }
                
                ...
            }
        )
        
        self.dataSource = dataSource
    }

NSTabInfoProvider.swift

import Foundation
import CoreData

// We are using https://github.com/yccheok/earthquakes-WWDC20 as gold reference.
class NSTabInfoProvider {
    
    weak var fetchedResultsControllerDelegate: NSFetchedResultsControllerDelegate?
    
    lazy var fetchedResultsController: NSFetchedResultsController<NSTabInfo> = {
        
        let fetchRequest = NSTabInfo.fetchSortedRequest()
        
        // Create a fetched results controller and set its fetch request,context,and delegate.
        let controller = NSFetchedResultsController(
            fetchRequest: fetchRequest,managedobjectContext: CoreDataStack.INSTANCE.persistentContainer.viewContext,sectionNameKeyPath: nil,cacheName: nil
        )
        
        controller.delegate = fetchedResultsControllerDelegate
        
        // Perform the fetch.
        do {
            try controller.performFetch()
        } catch {
            error_log(error)
        }
        
        return controller
    }()
    
    var nsTabInfos: [NSTabInfo]? {
        return fetchedResultsController.fetchedobjects
    }
    
    init(_ fetchedResultsControllerDelegate: NSFetchedResultsControllerDelegate) {
        self.fetchedResultsControllerDelegate = fetchedResultsControllerDelegate
    }
    
    func getNSTabInfo(_ indexPath: IndexPath) -> NSTabInfo? {
        guard let sections = self.fetchedResultsController.sections else { return nil }
        return sections[indexPath.section].objects?[indexPath.item] as? NSTabInfo
    }
}

解决方法

我认为问题与模型可能是使用背景上下文添加或更新的事实有关

lazy var backgroundContext: NSManagedObjectContext = {
    let backgroundContext = persistentContainer.newBackgroundContext()

    // TODO: Not sure these are required...
    //
    backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    //backgroundContext.undoManager = nil
    
    return backgroundContext
}()

这可能就是您需要将所有内容推送到主线程的原因,因为在您的方法中,您试图更新作为 UI 组件的数据源(通过扩展您的 tableview),因此它需要在主线程上。

您可以将主线程视为 UI 线程。

,

请注意运行时错误的突出显示部分。

'检测到死锁:不允许在具有未完成异步更新的主队列上调用此方法,并且会死锁。 请始终提交更新,要么始终在主队列中,要么始终不在主队列中

UICollectionViewDiffableDataSource.apply 文档中也明确提到了这一点。

讨论

diffable 数据源计算集合视图的当前状态与应用快照中的新状态之间的差异,这是一个 O(n) 操作,其中 n 是快照中的项目数。

您可以安全地从后台队列中调用此方法,但您必须在您的应用中始终如一地这样做。始终从主队列或后台队列专门调用此方法。

你需要做什么?

检查代码中 UICollectionViewDiffableDataSource.apply 的所有调用点,并确保它们始终处于关闭/打开主线程的状态。你不能从多个线程调用它(一次从主线程,一次从其他线程等)

,

我已经找到了问题的根本原因。

这是由于我对惰性初始化变量的理解不够。

有问题的代码

class NSTabInfoProvider {
    lazy var fetchedResultsController: NSFetchedResultsController<NSTabInfo> = {
        
        let fetchRequest = NSTabInfo.fetchSortedRequest()
        
        // Create a fetched results controller and set its fetch request,context,and delegate.
        let controller = NSFetchedResultsController(
            fetchRequest: fetchRequest,managedObjectContext: CoreDataStack.INSTANCE.persistentContainer.viewContext,sectionNameKeyPath: nil,cacheName: nil
        )
        
        controller.delegate = fetchedResultsControllerDelegate
        
        // Perform the fetch.
        do {
            try controller.performFetch()
        } catch {
            error_log(error)
        }
        
        return controller
    }()
}

self.nsTabInfoProvider = NSTabInfoProvider(self)
// Trigger performFetch
_ = self.nsTabInfoProvider.fetchedResultsController
  1. 在惰性变量初始化中触发 performFetch 是错误的。
  2. 因为那会触发回调。
  3. 回调可能会尝试访问 NSTabInfoProviderfetchedResultsController
  4. 但是 NSTabInfoProviderfetchedResultsController 完全初始化,因为代码尚未从惰性变量初始化范围返回。

固定代码

解决办法是

class NSTabInfoProvider {
    lazy var fetchedResultsController: NSFetchedResultsController<NSTabInfo> = {
        
        let fetchRequest = NSTabInfo.fetchSortedRequest()
        
        // Create a fetched results controller and set its fetch request,cacheName: nil
        )
        
        controller.delegate = fetchedResultsControllerDelegate
        
        return controller
    }()

    func performFetch() {
        do {
            try self.fetchedResultsController.performFetch()
        } catch {
            error_log(error)
        }
    }
}

self.nsTabInfoProvider = NSTabInfoProvider(self)
self.nsTabInfoProvider.performFetch()