存储对已删除 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
,在视图中加载调用了一些方法reloadData
和databaseDidChange
委托方法调用 reloadData
和 viewWillAppear
中的相同。
现在您的每个 DataBaseViewController
子类的视图控制器都将覆盖 reloadData
并且在该方法中您将再次从数据库中获取数据。您要么获取所有项目,要么获取单个项目。因此,对于那些单一的,您需要保存对象的id
并通过该id
再次获取它。如果返回的对象是 nil
,则项目已被删除,因此您发现了您似乎提到的问题。
所有这些事情都过于简单了,但我希望您对核心数据以及如何使用它有一个基本的了解。这并不容易,从来没有,而且很可能永远也不会。它旨在提高速度,甚至能够在尽可能短的时间内从非常大的数据库中访问数据。结果是它可能不是很安全。
【讨论】:
当您说:“一旦从数据库中检索到所有托管对象并复制其属性。”你是说当你使用 CoreData 时,一旦你从数据库中检索到你的对象,你实际上会将它们的所有属性复制到内存中的新的非托管对象中?因此,您需要为所有类创建两个版本:一个是托管的,一个是非托管的。 在我描述的情况下是的。但它适用于复杂的情况,这不是您通常需要的。一开始可能看起来很奇怪,但是一旦您看到这些对象不仅用于核心数据,还用于服务器数据(json 字典)和 UI 模型,可用于复杂系统(例如 MVVM)并且是完全线程安全的,您可能会发现它很方便。如果你正在考虑创建这个系统,我建议你有一个超类来完成大部分工作,那么子类只需要两种方法来从托管对象转换到托管对象。请注意,核心数据支持子类化以上是关于存储对已删除 NSManagedObject 的引用的局部变量会发生啥的主要内容,如果未能解决你的问题,请参考以下文章