MagicalRecord数据库与NSFetchedResultsController不一致

时间:2017-08-30 12:27:45

标签: ios core-data magicalrecord

我使用MagicalRecord作为我的项目,在我的数据库中我有CDSong实体,可以由多个CDVoter实体投票。 Database structure

使用从串行调度队列调用的NSManagedObjectContext.performAndWait(block:)在背景中添加和删除选民。我有一个NSFetchedResultsController来获取CDSongs并显示他们的选民(在这个简单的场景中它只打印选民姓名)。

一切都会好的,但我偶尔会在NSFetchedResultsControllerDelegate的controllerDidChangeContent方法中收到崩溃: - /根据我的分析,似乎有些无效的空CDVoter(name = nil,votedSong = nil)对象出现在CDSong中。选民关系。这些空选民不会从CDVoter.mr_findAll()返回。

这是模拟崩溃的代码(通常在< 20按钮点击应用程序崩溃后,因为CDVoter的名称为nil)。我在上下文和保存方面做错了吗?如果有人想尝试将整个测试代码放在数据库和frc初始化中,但有问题的部分是controllerDidChangeContentbuttonPressed方法。谢谢你的帮助:)

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 :)但没有成功。

1 个答案:

答案 0 :(得分:0)

好的,所以我找到了崩溃的原因:虽然mr_saveToPersistentStoreAndWait等待更改保存到rootSavingContext中,但它不会等到它们被合并到defaultContext中(如果它们是由私有的队列上下文)。如果在主队列上下文合并主线程上的旧更改之前,rootSavingContext已被另一个保存更改,则合并将被破坏(NSManagedObjectContextDidSave通知中的更改不对应于rootContextDidSave:中rootSavingContext的当前上下文状态MagicalRecord的内部方法)。

解释我提出的解决方案:

  1. DatabaseSavingManager包含一个私有队列保存上下文,该上下文将用于应用程序中的所有保存(如果您想使用多个保存上下文,这可能是一个缺点,但它对我来说已经足够了需求 - 在后台进行保存并保持一致性)。正如@Sneak评论的那样,没有理由使用后台串行队列来创建多个上下文并等待它们完成(这是我最初做的),因为NSManagedObjectContext有自己的串行队列,所以现在我使用了一个在主线程上创建的上下文,因此必须始终从主线程调用(使用perform(block:)来避免主线程阻塞)。

  2. 保存到持久存储后,保存上下文等待来自defaultContext的NSManagedObjectContextObjectsDidChange通知,以便它知道defaultContext已合并更改。这就是为什么不允许使用除DatabaseSavingManager的保存上下文之外的其他保存,因为它们可能会混淆等待过程。

  3. 以下是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
                    }
                }
            }
        }
    

    当然,这不是最优雅的解决方案,只是我想到的第一个,所以欢迎其他方法