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

Why DispatchQueue.main.async is required when using CoreData, NSFetchedResultsController and Diffable Data Source

在处理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 }
        }
    }
}

但是,我们在viewDidLoad运行performFetch之后,在dataSource.apply

会出现如下错误

'Deadlock detected: calling this method on the main queue with outstanding async updates is not permitted and will deadlock. Please always submit updates either always on the main queue or always off the main queue

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

申请后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 数据源时需要 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 组件,因此它需要在主线程上。

你可以把主线程想象成一个UI线程。

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

'Deadlock detected: calling this method on the main queue with outstanding async updates is not permitted and will deadlock. Please always submit updates either always on the main queue or always off the main queue

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

Discussion

The diffable data source computes the difference between the collection view’s current state and the new state in the applied snapshot, which is an O(n) operation, where n is the number of items in the snapshot.

You can safely call this method from a background queue, but you must do so consistently in your app. Always call this method exclusively from the main queue or from a background queue.

你需要做什么?

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

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

这是我对lazy initialized variable理解不足造成的

有问题的代码

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 完全初始化,因为代码尚未 return 从惰性变量初始化范围.

固定码

解决方案是

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
        
        return controller
    }()

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

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