奇怪的NSManagedObject行为

我遇到了奇怪的CoreData问题。
首先,在我的项目中,我使用了很多的框架,所以有很多问题的根源 – 所以我认为要创build一个最小的项目,重复我的问题。 您可以在Github上克隆testing项目,并逐步重复我的testing。
所以,问题是:
NSManagedObject绑定到NSManagedObjectID,它不会让对象从NSManagedObjectContext正确删除
所以,重现步骤:
在我的AppDelegate中,我像往常一样设置CoreData堆栈。 AppDelegate具有managedObjectContext属性,可以访问它来获取主线程的NSManagedObjectContext。 应用程序的对象图由一个实体Messagebodyfromtimestamp属性组成。 应用程序只有一个只有viewDidLoad方法的viewController。 它看起来如此:

 - (void)viewDidLoad { [super viewDidLoad]; NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext; NSEntityDescription *messageEntity = [NSEntityDescription entityForName:NSStringFromClass([Message class]) inManagedObjectContext:context]; // Here we create message object and fill it Message *message = [[Message alloc] initWithEntity:messageEntity insertIntoManagedObjectContext:context]; message.body = @"Hello world!"; message.from = @"Petro Korienev"; NSDate *now = [NSDate date]; message.timestamp = now; // Now imagine that we send message to some server. Server processes it, and sends back new timestamp which we should assign to message object. // Because working with managed objects asynchronously is not safe, we save context, than we get it's objectId and refetch object in completion block NSError *error; [context save:&error]; if (error) { NSLog(@"Error saving"); return; } NSManagedObjectID *objectId = message.objectID; // Now simulate server delay double delayInSeconds = 5.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { // Refetch object NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext; Message *message = (Message*)[context objectWithID:objectId]; // here i suppose message to be nil because object is already deleted from context and context is already saved. message.timestamp = [NSDate date]; // However, message is not nil. It's valid object with data fault. App crashes here with "Could not fulfill a fault" NSError *error; [context save:&error]; if (error) { NSLog(@"Error updating"); return; } }); // Accidentaly user deletes message before response from server is returned delayInSeconds = 2.0; popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { // Fetch desired managed object NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"timestamp == %@", now]; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Message class])]; request.predicate = predicate; NSError *error; NSArray *results = [context executeFetchRequest:request error:&error]; if (error) { NSLog(@"Error fetching"); return; } Message *message = [results lastObject]; [context deleteObject:message]; [context save:&error]; if (error) { NSLog(@"Error deleting"); return; } }); } 

那么,我检测到应用程序崩溃,所以我尝试以另一种方式获取message 。 我更改了抓取代码:

 ... // Now simulate server delay double delayInSeconds = 5.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { // Refetch object NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"timestamp == %@", now]; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Message class])]; request.predicate = predicate; NSError *error; NSArray *results = [context executeFetchRequest:request error:&error]; if (error) { NSLog(@"Error fetching in update"); return; } Message *message = [results lastObject]; NSLog(@"message %@", message); message.timestamp = [NSDate date]; [context save:&error]; if (error) { NSLog(@"Error updating"); return; } }); ... 

哪个NSLog'ed message (null)
所以,它显示:
1)消息实际上不存在于数据库中。 它不能被提取。
2)代码的第一个版本保持在上下文中删除message对象(可能是因为它的对象ID保留块调用)。
但是,为什么我可以通过它的ID获得删除的对象? 我需要知道。
显然,首先,我把objectId改为__weak 。 甚至在块之前崩溃:)
在这里输入图像说明

那么CoreData是否没有ARC? 嗯有趣。
那么,我考虑copy NSManagedObjectID。 我得到了什么?
在这里输入图像说明

 (lldb) po objectId 0xc28ed20 <x-coredata://8921D8F8-436C-4CBC-B4AB-118198988D88/Message/p4> (lldb) po message.objectID 0xc28ed20 <x-coredata://8921D8F8-436C-4CBC-B4AB-118198988D88/Message/p4> 

看看有什么不对? NSCopying-copy是像NSManagedObjectID return self一样实现的
对于objectId,最后的尝试是__unsafe_unretained 。 开始了:

 ... __unsafe_unretained NSManagedObjectID *objectId = message.objectID; Class objectIdClass = [objectId class]; // Now simulate server delay double delayInSeconds = 5.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { if (![NSObject safeObject:objectId isMemberOfClass:objectIdClass]) { NSLog(@"Object for update already deleted"); return; } ... 

safeObject:isMemberOfClass:实现:

 #ifndef __has_feature #define __has_feature(x) 0 #endif #if __has_feature(objc_arc) #error ARC must be disabled for this file! use -fno-objc-arc flag for compile this source #endif #import "NSObject+SafePointer.h" @implementation NSObject (SafePointer) + (BOOL)safeObject:(id)object isMemberOfClass:(__unsafe_unretained Class)aClass { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-objc-isa-usage" return ((NSUInteger*)object->isa == (NSUInteger*)aClass); #pragma clang diagnostic pop } @end 

简单的解释 – 我们使用__unsafe_unretainedvariables,所以在块调用时它可以被释放,所以我们必须检查它是否是有效的对象。 所以我们把它保存在块之前的class (它不是保留,它是分配),并通过safePointer:isMemberOfClass:在块中检查safePointer:isMemberOfClass:
所以现在,通过它的managedObjectId来重新获取对象是我的UNTRUSTED模式。
有没有人有这样的build议我应该怎么做? 要使用__unsafe_unretained并检查? 但是,这个managedObjectId也可以被其他代码保留,所以会导致could not fulfill属性访问的崩溃。 或者每次通过谓词来获取对象? (以及如果对象由3-4个属性唯一定义,该怎么办?保留所有完成块?)。 什么是asynchronous处理pipe理对象的最佳模式?
对不起,长期的研究,在此先感谢。

PS您仍然可以重复我的步骤或使用Test项目进行自己的实验

不要使用objectWithID: 使用existingObjectWithID:error: 。 根据文件, 前者 :

…总是返回一个对象。 假设objectID表示的持久性存储中的数据存在 – 如果不存在,则在访问任何属性(即,触发故障)时,返回的对象将引发exception。 这种行为的好处是,它允许您创build和使用错误,然后在稍后或单独的上下文中创build基础数据。

这正是你所看到的。 你得到一个对象,因为核心数据认为你必须要有一个这个ID,即使它没有。 当你尝试存储它,而不是在这个过渡期间创build一个实际的对象,它不知道该怎么做,你会得到exception。

existingObject...只会返回一个对象。