在Swift 5中配置核心数据持久化

9

我正在使用以下教程将Core Data集成到我的Swift IOS应用程序中。如视频所示,我的持久性管理器是通过单例模式创建的。以下是描述它的代码:

import Foundation
import CoreData

class DataLogger {

    private init() {}
    static let shared = DataLogger()
    lazy var context = persistentContainer.viewContext

    private var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "mycoolcontainer")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                print("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    func save () {
        if self.context.hasChanges {
            self.context.perform {
                do {
                    try self.context.save()
                } catch {
                    print("Failure to save context: \(error)")
                }
            }
        }
    }
}

现在,如果我使用以下代码创建一个包含大约1000个MyEntity实体对象( MyEntity是Core Data实体对象 )的循环,应用程序会崩溃。

class MySampleClass {

    static func doSomething {
        for i in 0 ..< 1000 {
            let entry = MyEntity(context: DataLogger.shared.context);
            // do random things
        }
        DataLogger.shared.save()
    }
}

在我的代码中,出现了崩溃情况:MyEntity(context: DataLogger.shared.context),我尝试查看日志以便排查错误,但是未能成功。有时候,在调用save()方法时,它会成功地执行或者崩溃,并出现以下通用错误:

发现堆破坏,free list的指针已经受到损坏,地址为0x280a28240 *** 错误的保护值: 13859718129998653044

我已经尝试在DataLogger中通过`.performAndWait()`方法来同步保存(synchronized)数据,但是没有成功。我还尝试使用childContexts方法来实现相同的功能,但也没有成功,以下是相关代码:

let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
childContext.parent = context
childContext.hasChanged {
    childContext.save();
}

我怀疑我实现DataLogger类的方式不正确,但无法观察到实际问题可能是什么。它可能与创建的对象数量有关,或者可能与线程有关,但我不确定。如何正确实现DataLogger类以确保任何其他类都可以使用它并将实体存储到磁盘上?


1
尝试将persistentContainer也初始化为lazy - vadian
@vadian 尝试了,但没有成功。 - angryip
如果您能上传一个示例项目,那么修复它将会更加容易。 - J. Doe
我只编写了足够的代码来运行您提供的项目代码。它成功运行了。 - J. Doe
3个回答

7

我最终花了更多的时间阅读Core Data,并采用以下方法解决了问题:

首先,我将持久性容器移到了应用程序委托中。可以定义如下:

 import UIKit
 import CoreData

 @UIApplicationMain
 class AppDelegate: UIResponder, UIApplicationDelegate { 

      ... applicationWillTerminate ....

      // add the core data persistance controller
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "mycoolcontainer")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                print("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container;
    }()

    func save() {
        let context = persistentContainer.viewContext;
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                print("Failure to save context: \(error)")
            }
        }
    }

 }

在我需要访问上下文的每个视图控制器中,我会按照以下方式执行操作:
import UIKit

class CoolViewController: UIPageViewController, UIPageViewControllerDelegate {

    let appDelegate = UIApplication.shared.delegate as! AppDelegate;

    func storeAndFetchRecords() {
        MySampleClass().doSomething(context: appDelegate.persistentContainer.viewContext);
        appDelegate.save()
    }

}

需要与上下文进行交互的类,应按以下方式执行:
import CoreData

class MySampleClass {

    func doSomething(context: NSManagedObjectContext) {
        for i in 0 ..< 1000 {
            let entry = MyEntity(context: context);
            // do random things
        }
    }
}

由于此内容存在于主线程中,因此无需运行“performAndWait”。仅在通过此语法启动后台上下文的情况下才有意义:
appDelegate.persistentContainer.newBackgroundContext()

6
我决定再添加一个答案,因为这种方法与我的先前答案不同。通过进一步阅读您问题的内容,发现内存分配存在问题。简而言之,我的评估是... 1.您的迭代 for i in 0 ..< 1000 在形式上是同步过程,但由于其中调用了核心数据,可能会与核心数据处理“失去同步”。我怀疑在 for ... in 迭代中正在进行的任何核心数据处理都无法在下一次迭代开始时完成,因此您的代码会遭受非法内存覆盖的影响。 或者 2.对于1000个实体,您对.save 的调用会对性能造成巨大的“打击”,并且根据您在同时运行的其他进程(特别是修改 NSManagedObjectContext 的进程)的情况,您可能会耗尽系统资源。我怀疑,根据您在调用.save 时在代码中运行的“其他”进程,您可能只是缺少系统资源,并且内存覆盖可能是一种机制,虽然失败,但试图让iOS“跟上”。

是的,我在这里涵盖了很多内容,但是试图指出一个可以帮助解决您问题的方向。

也许您的问题变成了...如何确保我的迭代中的Core Data进程在步进和/或保存之前完成?

我阅读过的所有关于Core Data的书籍和其他信息都表明,在NSManagedObjectContext中的操作是便宜的,而保存则是昂贵的。

以下是一本书的直接引用,我认为如果我引用作者,那么在这个网站上是被允许的...

“需要注意的是,无论是插入新对象到上下文中还是更改现有对象,都非常便宜 - 这些操作仅涉及托管对象上下文。插入和更改都在上下文中进行,而不会触及SQLite层。

只有在将这些更改保存到存储中时,成本相对较高。保存操作涉及协调器层和SQL层。因此,调用save()相当昂贵。

关键是要尽量减少保存次数。但是需要取得平衡:保存大量更改会增加内存使用,因为上下文必须跟踪这些更改直到它们被保存。处理大量更改需要比处理小更改更多的CPU和内存。然而,总体而言,一个大的更改集比许多小更改集更便宜。”

摘自:“Core Data.” by Florian Kugler and Daniel Eggert. (objc.io)

我现在没有明确的答案,因为坦率地说,这需要我花费一些时间和精力来解决,但是首先我建议你:

  • 学习性能调优 Core Data;
  • 研究将您对 MyEntity(context:) 的调用剥离到最基本的内容,例如创建具有 relationship 错误的实体或仅在内存中调用那些代码所需的 attributes
  • 研究使用 NSOperationOperation 来排队您的任务;
  • 观看 WWDC2015 上关于 "Advanced NSOperations" 的讲座。

如果您感兴趣,可以进一步阅读 SO:


我点赞是因为这是一篇很好的分析,安德鲁。然而,由于我需要一个实际的解决方案来解决问题,我想是时候提供一些悬赏来解决问题了。我的耐心已经消失了,哈哈。 - angryip
1
其他可考虑的可能性... 将数据构建成内存结构,如“字典”,使用“Struct”,然后通过单独的迭代对“MyEntity”进行填充/修改,使用后台线程或子上下文,并实现具有完成闭包的调用... 此完成将是.save的调用。 - andrewbuilder
我最后就是这样做的。虽然它有助于减少问题,但仍然会偶尔崩溃。:/ 但肯定离成功更近了。 - angryip

0

试试这个...

class MySampleClass {

    let dataLogger = DataLogger.shared
    let context = dataLogger.context

    static func doSomething {
        for i in 0 ..< 1000 {
            let entry = MyEntity(context: context);
            // do random things
        }
        dataLogger.save()
    }
}

通过在您的类中实例化DataLogger Singleton引用一次并将属性设置为其上下文,您将持有一个本地引用,应该保存对您的DataLogger类的重复调用。
以下是一个“副问题”评论 - 我不认为它是您问题的一部分 - 但是...我不确定您是否需要调用self.context.perform {}。其他人可能更了解如何解释为什么。个人会进行一些研究,并在弄清楚原因后更新我的答案。

尝试了这个,但没有成功。按照帖子中描述的情况仍然会崩溃。不过是一个聪明的想法。 - angryip

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接