我使用MagicalRecord作为我的项目,在我的数据库中我有CDSong实体,可以由多个CDVoter实体投票。
使用从串行调度队列调用的NSManagedObjectContext.performAndWait(block:)
在背景中添加和删除选民。我有一个NSFetchedResultsController
来获取CDSongs并显示他们的选民(在这个简单的场景中它只打印选民姓名)。
一切都会好的,但我偶尔会在NSFetchedResultsControllerDelegate的controllerDidChangeContent方法中收到崩溃: - /根据我的分析,似乎有些无效的空CDVoter(name = nil,votedSong = nil)对象出现在CDSong中。选民关系。这些空选民不会从CDVoter.mr_findAll()
返回。
这是模拟崩溃的代码(通常在< 20按钮点击应用程序崩溃后,因为CDVoter的名称为nil)。我在上下文和保存方面做错了吗?如果有人想尝试将整个测试代码放在数据库和frc初始化中,但有问题的部分是controllerDidChangeContent
和buttonPressed
方法。谢谢你的帮助:)
import UIKit
import CoreData
import MagicalRecord
class MRCrashViewController : UIViewController, NSFetchedResultsControllerDelegate {
var frc: NSFetchedResultsController<NSFetchRequestResult>!
let dispatchQueue = DispatchQueue(label: "com.testQueue")
override func viewDidLoad() {
super.viewDidLoad()
self.initializeDatabase()
self.initializeFrc()
}
func initializeDatabase() {
MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.error)
MagicalRecord.setupCoreDataStack()
MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.warn)
if CDSong.mr_findFirst() == nil {
for i in 1...5 {
let song = CDSong.mr_createEntity()!
song.id = Int16(i)
}
}
NSManagedObjectContext.mr_default().mr_saveToPersistentStoreAndWait()
}
func initializeFrc() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "CDSong")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: nil)
self.frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: NSManagedObjectContext.mr_default(), sectionNameKeyPath: nil, cacheName: nil)
self.frc!.delegate = self
try! self.frc!.performFetch()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
for song in controller.fetchedObjects! {
print((song as! CDSong).voters!.reduce("", { $0 + ($1 as! CDVoter).name! }))
}
print("----");
}
@IBAction func buttonPressed(_ sender: Any) {
for _ in 1...10 {
self.dispatchQueue.async {
let moc = NSManagedObjectContext.mr_()
moc.performAndWait {
for song in CDSong.mr_findAll(in: moc)! {
let song = song as! CDSong
let voters = song.voters!
for voter in voters {
(voter as! CDVoter).mr_deleteEntity(in: moc)
}
for _ in 1...4 {
if arc4random()%2 == 0 {
let voter = CDVoter.mr_createEntity(in: moc)!
voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65)))
voter.votedSong = song
}
}
}
moc.mr_saveToPersistentStoreAndWait()
}
}
}
}
}
注意: 我试图使用MagicalRecord.save(blockAndWait :)但没有成功。
答案 0 :(得分:0)
好的,所以我找到了崩溃的原因:虽然mr_saveToPersistentStoreAndWait
等待更改保存到rootSavingContext中,但它不会等到它们被合并到defaultContext中(如果它们是由私有的队列上下文)。如果在主队列上下文合并主线程上的旧更改之前,rootSavingContext已被另一个保存更改,则合并将被破坏(NSManagedObjectContextDidSave通知中的更改不对应于rootContextDidSave:
中rootSavingContext的当前上下文状态MagicalRecord的内部方法)。
解释我提出的解决方案:
DatabaseSavingManager
包含一个私有队列保存上下文,该上下文将用于应用程序中的所有保存(如果您想使用多个保存上下文,这可能是一个缺点,但它对我来说已经足够了需求 - 在后台进行保存并保持一致性)。正如@Sneak评论的那样,没有理由使用后台串行队列来创建多个上下文并等待它们完成(这是我最初做的),因为NSManagedObjectContext有自己的串行队列,所以现在我使用了一个在主线程上创建的上下文,因此必须始终从主线程调用(使用perform(block:)
来避免主线程阻塞)。
保存到持久存储后,保存上下文等待来自defaultContext的NSManagedObjectContextObjectsDidChange
通知,以便它知道defaultContext已合并更改。这就是为什么不允许使用除DatabaseSavingManager
的保存上下文之外的其他保存,因为它们可能会混淆等待过程。
以下是DatabaseSavingManager
的代码:
import Foundation
import CoreData
class DatabaseSavingManager: NSObject {
static let shared = DatabaseSavingManager()
fileprivate let savingDispatchGroup = DispatchGroup()
fileprivate var savingDispatchGroupEntered = false
fileprivate lazy var savingContext: NSManagedObjectContext = {
if !Thread.current.isMainThread {
var context: NSManagedObjectContext!
DispatchQueue.main.sync {
context = NSManagedObjectContext.mr_()
}
return context
}
else {
return NSManagedObjectContext.mr_()
}
}()
override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(defaultContextDidUpdate(notification:)), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: NSManagedObjectContext.mr_default())
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func save(block: @escaping (NSManagedObjectContext) -> ()) {
guard Thread.current.isMainThread else {
DispatchQueue.main.async {
self.save(block: block)
}
return
}
let moc = self.savingContext
self.savingContext.perform {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
}
}
func saveAndWait(block: @escaping (NSManagedObjectContext) -> ()) {
if Thread.current.isMainThread {
self.savingContext.performAndWait {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
}
}
else {
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.savingContext.perform {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
group.leave()
}
}
group.wait()
}
}
fileprivate func saveToPersistentStoreAndWait() {
if self.savingContext.hasChanges {
self.savingDispatchGroupEntered = true
self.savingDispatchGroup.enter()
self.savingContext.mr_saveToPersistentStoreAndWait()
self.savingDispatchGroup.wait()
}
}
@objc fileprivate func defaultContextDidUpdate(notification: NSNotification) {
if self.savingDispatchGroupEntered {
self.savingDispatchGroup.leave()
self.savingDispatchGroupEntered = false
}
}
}
示例如何使用它(不再NSFetchedResultsController
崩溃;可以从任何线程调用,也非常频繁):
DatabaseSavingManager.shared.save { (moc) in
for song in CDSong.mr_findAll(in: moc)! {
let song = song as! CDSong
let voters = song.voters!
for voter in voters {
(voter as! CDVoter).mr_deleteEntity(in: moc)
}
for _ in 1...4 {
if arc4random()%2 == 0 {
let voter = CDVoter.mr_createEntity(in: moc)!
voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65)))
voter.votedSong = song
}
}
}
}
当然,这不是最优雅的解决方案,只是我想到的第一个,所以欢迎其他方法