为什么在使用 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
感到困惑,因为
performFetch
在主线程中是 运行。
- 回调
didChangeContentWith
在主线程中 运行。
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
- 惰性变量初始化触发
performFetch
是错误的
- 因为那会触发回调。
- 回调可能会尝试访问
NSTabInfoProvider
的 fetchedResultsController
。
- 但
NSTabInfoProvider
的 fetchedResultsController
未 完全初始化,因为代码尚未 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()
在处理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
感到困惑,因为
performFetch
在主线程中是 运行。- 回调
didChangeContentWith
在主线程中 运行。 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
- 惰性变量初始化触发
performFetch
是错误的 - 因为那会触发回调。
- 回调可能会尝试访问
NSTabInfoProvider
的fetchedResultsController
。 - 但
NSTabInfoProvider
的fetchedResultsController
未 完全初始化,因为代码尚未 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()