存储对已删除 NSManagedObject 的引用的局部变量会发生啥

Posted

技术标签:

【中文标题】存储对已删除 NSManagedObject 的引用的局部变量会发生啥【英文标题】:What happens to local variables storing references to deleted NSManagedObjects存储对已删除 NSManagedObject 的引用的局部变量会发生什么 【发布时间】:2017-05-05 05:45:24 【问题描述】:

当我从数据库中删除 NSMangedObject 时,分配给它的局部变量会发生什么情况?

例如,我有一个简单的 NSManagedObject:

class MyManagedObject: NSManagedObject 
    @NSManaged var name: String

然后在我的 ViewController 中,我将其从数据库中拉出,并在本地分配:

class ViewController: UIViewController 
     var myManagedObject: MyManagedObject!

然后我从数据库中删除它。

如果打印对象名称,我会在控制台中得到以下内容

print("myManagedObject.name = \(myManagedObject.name)")
//prints: "myManagedObject.name = "

好像对象不存在一样?但是,如果我将变量转换为可选项并检查它是否为零,我会被告知它不是零。

我不太确定如何在我的脑海中调和这一点。似乎有一些东西指向了局部变量,但它的属性已经消失了。

如果我有许多不同的 UI 对象依赖于该对象的属性,我不能假设内存中存在它的一些本地深层副本?


这里是更完整的代码:

在 viewDidLoad 我创建新对象,保存上下文,获取对象,然后在本地分配它。

class ViewController: UIViewController 

      var myManagedObject: MyManagedObject!

      override func viewDidLoad() 
            super.viewDidLoad()

            //1 Create the new object
            let newObject = NSEntityDescription.insertNewObject(forEntityName: "MyManagedObject", into: coreDataManager.mainContext) as! MyManagedObject
            newObject.name = "My First Managed Object"

            //2 Save it into the context
            do 
                try coreDataManager.mainContext.save()
             catch 
                //handle error
             

            //3 Fetch it from the database
            let request = NSFetchRequest<MyManagedObject>(entityName: "MyManagedObject")
            do 
                let saved = try coreDataManager.mainContext.fetch(request)
                //4 Store it in a local variable
                self.myManagedObject = saved.first
             catch 
                 //handle errors
            
      

此时如果我打印局部变量的name 属性,我会得到正确的响应:

print("The object's name is: \(myManagedObject.name)")
//prints: The object's name is: My First Managed Object

所以,现在我从数据库中删除它:

if let storedObject = myManagedObject  
     coreDataManager.mainContext.delete(storedObject)
     do 
         try coreDataManager.mainContext.save()
      catch 
         //handle error
     

但是现在,当我打印时,我得到了最奇怪的输出:

print("myManagedObject.name = \(myManagedObject.name)")
//prints: "myManagedObject.name = "

这完全不是我期望记忆的工作方式。如果我创建一个类Foo 的实例,然后将该实例传递给不同的对象,那么它就是同一个实例。只有当没有人指向它时它才会消失。

在这种情况下——变量myManagedObject是什么?这不是nil。字符串name 是什么?它是一个空字符串吗?还是其他一些奇怪的元类型?

【问题讨论】:

显示您在本地“分配”它的代码。 var myManagedObject: MyManagedObject!只会“制作”一个 MyManagedObject 类型的新变量 由于您的问题更新:删除托管对象后将其标记为已删除,并在保存数据库后将其从数据库中删除(不在内存中)。在托管对象上,您应该检查 isDeleted 属性甚至“故障”属性。正如您预测的那样,该对象在内存中持续存在,但在您的情况下它的行为是不可预测的。根据框架的变化,任何事情都可能发生,空字符串似乎是一个很好的解决方法,它仍然可以工作。删除后不要使用此对象是您的工作。 但是兔子洞更深了。如果您使用多个上下文并在一个上下文中删除该对象,它仍将存在于另一个上下文中。但是,一旦您尝试在第二个上下文中应用更改,就会报告您需要解决的冲突。因此,在内存中,每个上下文都有一个实例,这些实例将保留在内存中,直到 ARC 规则决定释放它。但是它的属性可以随时改变。因此,该对象仍然存在,但其属性不可访问。无论如何,它应该崩溃,但似乎已经为您处理了异常。 @MatikOblak - 我相信以上两个 cmets 是我一直在寻找的答案。如果您想将它们移至您的答案,我会将其标记为已接受。事实上,检查 isFault 属性告诉我我需要知道什么,尽管我仍然不清楚为什么我不崩溃。在不崩溃的情况下找到错误将非常困难!我尝试检查 name == "" 是否正确,所以 CoreData 给了我一个空字符串……哎呀!我还认为,正如您所建议的那样,在我的情况下,复制内存中的内容最适合我的使用。谢谢。 由于您的问题不是关于如何检测已删除的托管对象,所以我们就保持这种方式。关于发现错误:您是否尝试过覆盖 setter?如果您确保研究如何在核心数据中做到这一点。尽管在您的情况下,您可能想要覆盖并将断点设置到故障设置器中,以找出删除它的原因。仍然可能行不通,如果不行,您需要添加观察者。 【参考方案1】:

您可能在这里寻找的主要内容是核心数据上下文。上下文是您的内存和实际数据库之间的连接。

每当您获取数据时,您都会通过上下文获取数据。这些是可以修改甚至删除的托管对象。在您保存上下文之前,这些仍然没有真正发生在数据库上。

当你删除一个对象时,它被标记为删除,但它没有从内存中删除,它一定不会,因为如果没有别的,它仍然会被上下文用来从数据库本身中实际删除对象。

调用删除托管对象后,托管对象会发生什么情况几乎无关紧要,即使记录在案,它也可能会发生变化,因为它是框架的一部分。因此,您有责任检查这些情况并在需要时重新获取对象。因此,您必须确保您的应用程序具有适当的架构并负责任地使用核心数据。

您使用数据库的方式有很多种,并且或多或少都有一种独特的方式来优化使用它。您需要更具体地说明您正在做什么以及在哪里发现潜在问题,这样我们才能让您走上正轨。

举个例子,考虑从远程服务器同步数据。在这里,您期望数据可以随时同步,无论用户在做什么或他是应用程序的哪个部分。

为此,我建议您有一个在单独线程上运行的单一上下文。一旦从数据库中检索到,所有托管对象都应该被包装并复制其属性。在大多数实体上,您会有类似的内容:

MyEntity.findAll  items in
    ...the fetch happens on context thread and returns to main, items are wrappers

MyEntity.find(id: idString,  item in
    ...the fetch happens on context thread and returns to main, items are wrappers
)()

那么由于您无法直接访问托管对象,因此您需要某种方法将数据复制到托管对象,例如:

myEntityInstance.commit() // Copies all the data to core data object. The operation is done on a context thread. A callback is usually not needed

然后去保存数据库

MyEntity.saveDatabse 
   ... save happens on the context thread and callback is called on main thread

现在最聪明的部分是saveDatabse 方法将向委托报告已进行更改。委托通常是当前视图控制器,因此有一个像DataBaseViewController 这样的超类是有意义的,它在视图中确实出现了将自己分配为委托MyEntity.delegate = self,在视图中加载调用了一些方法reloadDatadatabaseDidChange委托方法调用 reloadDataviewWillAppear 中的相同。

现在您的每个 DataBaseViewController 子类的视图控制器都将覆盖 reloadData 并且在该方法中您将再次从数据库中获取数据。您要么获取所有项目,要么获取单个项目。因此,对于那些单一的,您需要保存对象的id 并通过该id 再次获取它。如果返回的对象是 nil,则项目已被删除,因此您发现了您似乎提到的问题。

所有这些事情都过于简单了,但我希望您对核心数据以及如何使用它有一个基本的了解。这并不容易,从来没有,而且很可能永远也不会。它旨在提高速度,甚至能够在尽可能短的时间内从非常大的数据库中访问数据。结果是它可能不是很安全。

【讨论】:

当您说:“一旦从数据库中检索到所有托管对象并复制其属性。”你是说当你使用 CoreData 时,一旦你从数据库中检索到你的对象,你实际上会将它们的所有属性复制到内存中的新的非托管对象中?因此,您需要为所有类创建两个版本:一个是托管的,一个是非托管的。 在我描述的情况下是的。但它适用于复杂的情况,这不是您通常需要的。一开始可能看起来很奇怪,但是一旦您看到这些对象不仅用于核心数据,还用于服务器数据(json 字典)和 UI 模型,可用于复杂系统(例如 MVVM)并且是完全线程安全的,您可能会发现它很方便。如果你正在考虑创建这个系统,我建议你有一个超类来完成大部分工作,那么子类只需要两种方法来从托管对象转换到托管对象。请注意,核心数据支持子类化

以上是关于存储对已删除 NSManagedObject 的引用的局部变量会发生啥的主要内容,如果未能解决你的问题,请参考以下文章

NSManagedObject 删除无法正常工作

扩展 NSManagedObject 的删除方法

创建 NSManagedObject 属性值的副本

删除 NSManagedObject 时的附加操作

如何观察 NSManagedObject 是不是从 managedObjectContext 中移除

无论当前上下文状态如何,如何获取 NSManagedObject 的持久存储副本