0

在处理 CoreData、NSFetchedResultsController 和 Diffable 数据源时,我总是注意到我需要应用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 }
        }
    }
}

但是,在我们运行performFetchviewDidLoad,我会收到以下错误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 数据源时需要 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
    }
}
4

3 回答 3

0

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

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

有问题的代码

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. 回调可能会尝试访问NSTabInfoProvider.fetchedResultsController
  4. 但是NSTabInfoProvider'sfetchedResultsController没有完全初始化,因为代码还没有从惰性变量初始化范围返回

固定代码

解决方案是

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()
于 2021-06-16T09:02:33.597 回答
0

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

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

于 2021-06-16T07:20:35.463 回答
0

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

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

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

讨论

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

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

你需要做什么?

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

于 2021-06-16T08:00:11.090 回答