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

标签 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...)

最佳答案

TL;DR: 摆脱您的队列,将其替换为操作队列。使用 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:,这将为您提供一个使用上下文,并将执行队列中的每个 block ,这样您就不会发生写入冲突。确保没有 managedObject 离开这个 block - 它们附加到将在 block 末尾销毁的上下文。此外,您不能将 managedObjects 传递到此 block 中 - 如果您想更改 viewContext 对象,您必须使用 objectID 并在后台上下文中获取。为了在 viewContext 上看到更改,您必须添加到核心数据设置 persistentContainer.viewContext.automaticallyMergesChangesFromParent = true

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

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

关于ios - 在串行队列上获取 CoreData 期间崩溃,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42521141/

相关文章:

ios - 添加/删除 ViewController 作为 subview Swift 后,ParentView 属性设置为 nil

ios - 您可以在 iOS 应用程序中全局禁用旋转吗?

iphone - 如何保存两张图片,其中一张允许旋转、缩放和移动

Ios - 按下 UIButton 时更改 UIImageView 的图像

ios - 在 swift 4/Xcode 10 中第二次启动 App 时跳过初始 View Controller

ios - NSMutableArray 对副本的更改也会导致父 NSMutableArray 的更改

ios - MapKit 中特定位置的海拔高度

ios - 入口点 (_main) 未定义。对于体系结构 x86_64 - 仅限 XCode UITesting

ios - 如何组织tableview单元格

xcode - 分配给 'BOOL 的不兼容整数到指针转换