奇怪的NSManagedObject行为

时间:2013-09-27 22:34:38

标签: ios objective-c core-data

我遇到了奇怪的CoreData问题 首先,在我的项目中我使用了很多框架,所以有很多问题来源 - 所以我考虑创建一个重复我的问题的最小项目。您可以克隆Test project on Github并逐步重复我的测试 那么,问题是:
NSManagedObject绑定到它的NSManagedObjectID,它不允许从NSManagedObjectContext中正确删除对象
那么,重现的步骤:
在我的AppDelegate中,我像往常一样设置CoreData堆栈。 AppDelegate具有managedObjectContext属性,可以访问该属性以获取主线程的NSManagedObjectContext。应用程序的对象图由一个具有Messagebodyfrom属性的实体timestamp组成。 Application只有一个viewController,只有viewDidLoad方法。看起来如此:

- (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)DB中实际上不存在消息。它无法获取。
2)代码的第一个版本在上下文中保持删除message对象(可能导致它的对象id被保留用于块调用)。
但为什么我可以通过它的id获取已删除的对象?我需要知道。
显然,首先,我将objectId更改为__weak。甚至在阻止之前崩溃了:) enter image description here

所以CoreData是在没有ARC的情况下构建的吗?嗯有趣。
好吧,我考虑过copy NSManagedObjectID。我得到了什么? enter image description here

(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的{​​{1}}实施-copy return self 对于objectId,最后一次尝试是NSManagedObjectID。我们走了:

__unsafe_unretained

safeObject:isMemberOfClass:implementation:

...    
    __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;
        }
...        

简要说明 - 我们使用#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_unretained(它不会被保留,它被分配)并通过class在块中检查它 所以现在,通过它的managedObjectId重新获取对象是我的 UNTRUSTED 模式。
有没有人建议我在这种情况下该怎么做?要使用__unsafe_unretained并检查?但是,此managedObjectId也可以由其他代码保留,因此会导致safePointer:isMemberOfClass:在属性访问时崩溃。或者每次通过谓词获取对象? (如果对象由3-4个属性唯一定义,该怎么办?将它们全部保留为完成块?)。异步处理托管对象的最佳模式是什么? 对不起,长期研究,提前谢谢。

附:您仍然可以使用Test project

重复我的步骤或进行自己的实验

1 个答案:

答案 0 :(得分:2)

请勿使用objectWithID:。使用existingObjectWithID:error:。根据文档the former

  

...总是返回一个对象。持久性存储中的数据   假设存在objectID表示的 - 如果不存在,则表示存在   当您访问任何属性时,返回的对象会抛出异常(   是,当故障发生时)。这种行为的好处是它   允许您创建和使用故障,然后创建基础数据   稍后或在单独的背景下。

这正是你所看到的。你得到一个对象,因为Core Data认为你必须要一个具有该ID的对象,即使它没有。当你试图存储它时,没有在过渡期间创建一个实际的对象,它不知道该做什么,你得到了例外。

existingObject...只有在存在对象时才会返回该对象。