ios – 奇怪的NSManagedObject行为

我遇到了奇怪的CoreData问题.

首先,在我的项目中我使用了很多框架,所以有很多问题来源 – 所以我考虑创建一个重复我的问题的最小项目.您可以克隆
Test project on Github并逐步重复我的测试.

那么,问题是:

NSManagedObject绑定到它的NSManagedObjectID,它不允许从NSManagedObjectContext中正确删除对象

那么,重现的步骤:

在我的AppDelegate中,我像往常一样设置CoreData堆栈. AppDelegate具有managedObjectContext属性,可以访问该属性以获取主线程的NSManagedObjectContext.应用程序的对象图由一个实体Message和body,from,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;
        }
    });
}

好吧,我检测到应用程序崩溃,所以我尝试以另一种方式获取消息.我更改了获取代码:

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

那么CoreData是在没有ARC的情况下构建的嗯有趣.
好吧,我考虑复制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一样实现
最后一次尝试是object_d的__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:implementation:

#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变量,因此在块调用时它可以被释放,因此我们必须检查它是否是有效对象.所以我们在块之前保存它的类(它不是保留,它是分配)并通过safePointer在块中检查它:isMemberOfClass:
所以现在,通过它的managedObjectId重新获取对象对我来说是UNTRUSTED模式.
有没有人建议我在这种情况下该怎么做?要使用__unsafe_unretained并检查?但是,此managedObjectId也可以由另一个代码保留,因此它将导致无法实现属性访问的崩溃.或者每次通过谓词获取对象? (如果对象由3-4个属性唯一定义,该怎么办?将它们全部保留为完成块?).异步处理托管对象的最佳模式是什么?
对不起,长期研究,提前谢谢.
附:您仍然可以重复我的步骤或使用Test project进行自己的实验

最佳答案 不要使用objectWithID:.使用existingObjectWithID:error:.根据文档,
the former

… always returns an object. The data in the persistent store
represented by objectID is assumed to exist—if it does not, the
returned object throws an exception when you access any property (that
is, when the fault is fired). The benefit of this behavior is that it
allows you to create and use faults, then create the underlying data
later or in a separate context.

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

existingObject …仅在存在对象时返回对象.

点赞