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

时间:2017-03-01 00:24:08

标签: ios swift xcode core-data thread-safety

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

以下是上下文:我有一个应用程序,它必须对CoreData进行多次访问。为简化起见,我决定专门为访问声明一个串行线程(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...)

1 个答案:

答案 0 :(得分:3)

TL; DR:删除队列,将其替换为操作队列。使用viewContext在主线程上运行提取,以同步方式写入。

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

通过从不是主线程的线程访问viewContext导致崩溃。有一个队列确保没有其他任何东西同时访问核心数据的事实并没有解决它。当核心数据线程安全被违反时,核心数据可能随时 以任何方式失败。这意味着它可能会崩溃而难以诊断崩溃报告,即使在代码中您处于正确线程的位置也是如此。

您有正确的想法,核心数据需要队列才能在保存数据时正常运行,但您的实施存在缺陷。核心数据队列将防止因从不同上下文同时向实体写入冲突属性而导致的写入冲突。使用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。正如您通常阅读以向用户显示信息一样,您不会通过使用其他线程来获取任何信息。主线程必须在任何事件中等待信息,因此只需在主线程上运行fetch就会更快。永远不要写在viewContext上。 viewContext不使用操作队列,因此在其上写入可能会产生写入冲突。同样,您应该将您创建的任何其他上下文(使用newBackgroundContextperformBackgroundTask)视为只读,因为它们也将位于写入队列之外。

起初我认为NSPersistentContainer&#39; s performBackgroundTask有一个内部队列,初始测试支持它。经过更多测试后,我发现它也可能导致合并冲突。