【问题标题】:Why DispatchQueue.main.async is required when using CoreData, NSFetchedResultsController and Diffable Data Source为什么在使用 CoreData、NSFetchedResultsController 和 Diffable 数据源时需要 DispatchQueue.main.async
【发布时间】:2021-08-31 21:44:22
【问题描述】:

在处理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 中得到以下错误

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

我可以通过以下方式“解决”问题

应用 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 吗?

以下是我们的详细代码sn-p。

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
    }
}

【问题讨论】:

    标签: ios swift core-data nsfetchedresultscontroller uicollectionviewdiffabledatasource


    【解决方案1】:

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

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

    有问题的代码

    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, 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()
    

    【讨论】:

      【解决方案2】:

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

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

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

      讨论

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

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

      你需要做什么?

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

      【讨论】:

        【解决方案3】:

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

        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 线程。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-02-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多