问题描述
在处理 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
感到困惑,因为
-
performFetch
在主线程中运行。 - 回调
didChangeContentWith
在主线程中运行。 -
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
- 在惰性变量初始化中触发
performFetch
是错误的。 - 因为那会触发回调。
- 回调可能会尝试访问
NSTabInfoProvider
的fetchedResultsController
。 - 但是
NSTabInfoProvider
的fetchedResultsController
未完全初始化,因为代码尚未从惰性变量初始化范围返回。
固定代码
解决办法是
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()