在串行队列上获取 CoreData 期间崩溃

Posted

技术标签:

【中文标题】在串行队列上获取 CoreData 期间崩溃【英文标题】:Crashes during CoreData fetching on serial queue 【发布时间】:2017-03-01 00:24:07 【问题描述】:

我经历了很多关于 CoreData 的讨论和主题,但我一直遇到同样的问题。

这里是上下文:我有一个应用程序必须对 CoreData 进行多次访问。为了简化,我决定声明一个 serial 线程专门用于访问(queue.sync 用于获取,queue.async 用于保存)。我有一个嵌套三次的结构,为了重新创建整个结构,我获取 subSubObject,然后是 SubObject,最后是 Object

有时(如“对象”的 1/5000 再现)CoreData 在获取结果时崩溃,没有堆栈跟踪,没有崩溃日志,只有一个 EXC_BAD_ACCESS(代码 1)

对象不是原因,而且崩溃很奇怪,因为所有访问都是在 同一个线程中完成的,这是一个 串行线程

如果有人可以帮助我,我将非常感激!

代码结构如下:

private let delegate:AppDelegate
private let context:NSManagedObjectContext
private let queue:DispatchQueue

override init() 
    self.delegate = (UIApplication.shared.delegate as! AppDelegate)
    self.context = self.delegate.persistentContainer.viewContext
    self.queue = DispatchQueue(label: "aLabel", qos: DispatchQoS.utility)
    super.init()



(...)

public func loadObject(withID ID: Int)->Object? 

    var object:Object? = nil

    self.queue.sync 

        let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Name")
        fetchRequest.predicate = NSPredicate(format: "id == %@", NSNumber(value: ID))

        do 
            var data:[NSManagedObject]

            // CRASH HERE ########################
            try data = context.fetch(fetchRequest)
            // ###################################

            if (data.first != nil) 
                let subObjects:[Object] = loadSubObjects(forID: ID)
                // Task creating "object"
            
         catch let error as NSError 
            print("CoreData : \(error), \(error.userInfo)")
        
    

    return object


private func loadSubObjects(forID ID: Int)->[Object] 

    var objects:[Object] = nil

    self.queue.sync 

        let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Name")
        fetchRequest.predicate = NSPredicate(format: "id == %@", NSNumber(value: ID))

        do 
            var data:[NSManagedObject]

            // OR HERE ###########################
            try data = context.fetch(fetchRequest)
            // ###################################

            if (data.first != nil) 
                let subSubObjects:[Object] = loadSubObjects(forID: ID)
                // Task creating "objects"
            
         catch let error as NSError 
            print("CoreData : \(error), \(error.userInfo)")
        
    

    return objects


(etc...)

【问题讨论】:

How to init ManagedObjectContext on the right queue?的可能重复 在副本中查看我的答案。你不应该为 Core Data 使用自定义线程。您可以将其添加到您的项目中,以了解您将来何时违反线程:***.com/questions/31391838/… 【参考方案1】:

TL;DR:摆脱你的队列,用一个操作队列替换它。在主线程上运行 fetches,viewContext 以一种同步方式写入。

有两个问题。首先是 managedObjectContexts 不是线程安全的。除了设置为使用的单个线程之外,您无法访问上下文(既不能用于读取也不能用于写入)。第二个问题是您不应该同时对核心数据进行多次写入。同时写入可能会导致冲突和数据丢失。

崩溃是由非主线程的线程访问viewContext 引起的。有一个队列确保没有其他任何东西同时访问核心数据这一事实并不能解决这个问题。当违反核心数据线程安全性时,核心数据可能随时以任何方式失败。这意味着即使在代码中您位于正确线程上的点上,它也可能会因难以诊断崩溃报告而崩溃。

您的想法是正确的,即 core-data 在保存数据时需要一个队列才能正常工作,但是您的实现存在缺陷。核心数据队列将防止由于从不同上下文同时将冲突属性写入实体而导致的写入冲突。使用NSPersistentContainer 这很容易设置。

在您的核心数据管理器中创建一个 NSOperationQueue

let  persistentContainerQueue : OperationQueue = 
    let queue = OperationQueue.init();
    queue.maxConcurrentOperationCount = 1;
    return queue;
()

并使用此队列进行所有写入:

func enqueueCoreDataBlock(_ block: @escaping (NSManagedObjectContext) -> Swift.Void)
    persistentContainerQueue.addOperation 
        let context = self.persistentContainer.newBackgroundContext();
        context.performAndWait 
            block(context)
            do
                try context.save();
             catch
                //log error
            
        
    

对于写入使用enqueueCoreDataBlock:,它将为您提供使用的上下文,并将执行队列中的每个块,这样您就不会遇到写入冲突。确保没有 managedObject 离开此块 - 它们附加到将在块末尾销毁的上下文。此外,您不能将 managedObjects 传递到此块中 - 如果您想更改 viewContext 对象,您必须使用 objectID 并在后台上下文中获取。为了在viewContext 上看到更改,您必须添加到您的核心数据设置persistentContainer.viewContext.automaticallyMergesChangesFromParent = true

为了阅读,您应该使用主线程中的viewContext。正如您通常阅读以便向用户显示信息一样,通过使用不同的线程您不会获得任何东西。主线程在任何情况下都必须等待信息,因此在主线程上运行获取会更快。切勿在viewContext 上写字。 viewContext 不使用操作队列,因此在其上写入会产生写入冲突。同样,您应该将您创建的任何其他上下文(使用newBackgroundContext 或使用performBackgroundTask)视为只读,因为它们也将在写入队列之外。

起初我以为NSPersistentContainerperformBackgroundTask 有一个内部队列,初步测试支持这一点。经过更多测试,我发现它也可能导致合并冲突。

【讨论】:

非常有用,非常感谢!您应该写一个教程,因为我发现的所有内容都没有提到“viewContext”和“persistantContainer”之间的区别。我听说过“线程安全”问题,但现在很清楚了!现在由于更新我有几个问题,无论如何我将帖子标记为“已回答”:) Ps:所以,更新应该在“performBackgroundTask”中完成,对吗? (Ps2:对不起,这里是英语,法语开发者x)) 所有写入都应该使用performBackgroundTask完成 @JonRose big fetches 会导致 ui 冻结吗? 如果您有一个 ViewController 显示来自数据库的大量数据,那么在此期间您实际上没有太多其他事情可做,除了等待。如果您的提取时间少于 100 毫秒,那没关系。确实,您正在阻止 UI,但少于 100 毫秒是可以的。如果花费的时间比这更长,那么您的获取效率低下,应该修复。我从来没有遇到过提取需要很长时间并且无法修复但提高效率的情况。 乔恩·罗斯,我觉得看不清楚。我需要在 performBackgroundTask 中调用 enqueueCoreDataBlock 吗?或者我必须在 performBackgroundTask 中更改 NSManagedObjects?能否请您举例说明如何使用 performBackgroundTask 调用 enqueueCoreDataBlock?

以上是关于在串行队列上获取 CoreData 期间崩溃的主要内容,如果未能解决你的问题,请参考以下文章

属性名称更改期间的 CoreData 迁移问题

如何在同一线程上调度异步以进行串行处理

使用 CoreData 获取时崩溃

如何理解苹果文档中CoreData的父/子模型?

CoreData:错误:严重的应用程序错误。在核心数据更改处理期间捕获到异常

在 ReactJS 中获取 onComponentDidMount 期间出现 CORS 错误