【问题标题】:MagicalRecord database inconsistency with NSFetchedResultsControllerMagicalRecord 数据库与 NSFetchedResultsController 不一致
【发布时间】:2018-02-08 04:02:50
【问题描述】:

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

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

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

这是模拟崩溃的代码(通常在单击 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:) 没有成功。

【问题讨论】:

  • "选民在后台使用串行调度队列添加和删除" .对于核心数据,您不应该使用自己的队列。您应该使用 NSManagedObjectContext PerformBlock 方法。
  • @Sneak 我使用 performBlockAndWait 方法来处理核心数据,但我从 bg 队列中调用它们。我认为这没关系,至少docs 没有说我只能从主线程调用 performBlockAndWait,他们只说我应该从创建上下文的同一个线程调用该方法。我必须使用 bg 队列,因为如果从 main 调用 performBlockAndWait 会阻塞图形,并且仅调用 perform 会混合删除和插入(相信我,刚刚尝试过)。谢谢,我会修改句子
  • 删除了可以解决您问题的我的 cmets,而是输入此内容。既然你对你的“句子编辑”和“相信我”如此自大,而不是质疑我的观点并试图学习一些东西。祝你好运。
  • @Sneak 哦,伙计,对不起,如果听起来像那样,我不想争论,我质疑你的观点,研究了他们周围的文档并测试了可能的解决方案,这些解决方案只调用 performBlock 方法从主队列(我用“相信我”来表达我不只是拒绝可能性,而是真的尝试了它们)。而且我认为在stackoverflow中编辑误导性的句子是正常的甚至是期望的事情,不是吗?但是,我会根据您删除的 cmets 尝试一些新的想法,并让您知道,如果它们有效......
  • 不用担心,如果你想避免阻塞你的 UI,你应该用 NSPrivateQueueConcurrencyType 创建一个 NSManagedObjectContext ,然后做你的工作。您可以创建一个子上下文,在保存时将更改推送到您的主 moc。另外,不要使用 performBlockAndWait,它当然会阻塞你的 UI。这是一个很好的基础教程,你应该仔细阅读它。 raywenderlich.com/145877/… 除非出于某种原因您想要实现特定目标,否则请避免使用您自己的 CD 队列。

标签: ios core-data magicalrecord


【解决方案1】:

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

我提出的解决方案的解释:

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

  2. 保存到持久存储后,保存上下文等待来自 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
                }
            }
        }
    }

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

【讨论】:

  • 我厌倦了阅读你的答案,但我忘了提一个提示,打开多线程调试 oleb.net/blog/2014/06/core-data-concurrency-debugging 和你将知道您何时遇到线程问题。但是,快速浏览您的答案,您无需等待通知即可执行更多保存。而且您似乎仍在使用调度组和队列进行保存等。这仍然是不好的做法。这不是而且真的给你造成了混乱,我认为你真正的问题是你使用 MR 进行了修改,而不是深入研究核心数据是如何工作的。
  • 为了展示这一点,这实际上是你应该从 iOS 10+ 开始使用的 developer.apple.com/documentation/coredata/… 而不需要使用杂乱的手动跟踪通知编码和合并。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-06-22
  • 2011-12-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-22
  • 1970-01-01
相关资源
最近更新 更多