奇怪的NSManagedObject行为

4
我遇到了奇怪的CoreData问题。
首先,我的项目使用了很多框架,所以问题的来源很多 - 因此我考虑创建一个重复我的问题的最小项目。您可以克隆Github上的测试项目并逐步重复我的测试。
因此,问题如下:
NSManagedObject绑定了它的NSManagedObjectID,这使得对象无法从NSManagedObjectContext中正确删除
因此,重现步骤如下:
在我的AppDelegate中,我像往常一样设置了CoreData堆栈。AppDelegate有一个managedObjectContext属性,可以访问以获取主线程的NSManagedObjectContext。应用程序的对象图由一个实体Message组成,其中包含bodyfromtimestamp属性。 应用程序只有一个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打印了
因此,它显示:
1)消息实际上不存在于DB中。 它无法获取。
2)代码的第一个版本在某种程度上保留了已删除的message对象(可能是因为其对象ID被保留用于块调用)。
但是我如何通过其id获得已删除的对象? 我需要知道。
显然,首先,我将objectId更改为__weak。 甚至在块之前崩溃:)
enter image description here

因此,CoreData是没有使用ARC构建的? 嗯有趣。
好吧,我考虑复制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-copyNSManagedObjectID上被实现为return self
上次尝试使用__unsafe_unretained来处理objectId。这次我们来试试:

...    
    __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变量,因此在块调用时它可能被释放,所以我们必须检查它是否是有效的对象。因此,在块之前保存它的class(它没有保留,而是分配),并通过safePointer:isMemberOfClass:在块中进行检查。
因此,目前,按其managedObjectId重新获取对象是我不信任的模式。
有人有什么建议,我应该如何处理这种情况?继续使用__unsafe_unretained并检查吗?但是,这个managedObjectId也可以被另一个代码保留,所以它将导致在属性访问时could not fulfill崩溃。或者每次通过谓词获取对象?(如果对象由3-4个属性唯一定义,该怎么办?为完成块全部保留?)。使用异步管理对象的最佳模式是什么?
抱歉进行了长时间的研究,提前致谢。

P.S. 您仍然可以重复我的步骤或使用测试项目进行自己的实验。

我根据@Tommy的答案更新了仓库,包含正确的实现。 - Petro Korienev
1个回答

2
不要使用objectWithID:,而要使用existingObjectWithID:error:。根据文档,前者:

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

这正是你所看到的。你得到了一个对象,因为Core Data认为你必须要有一个具有该ID的对象,即使它没有一个。当你尝试将其存储时,没有在中间创建实际对象,它不知道该怎么做,因此你会得到异常。

existingObject...只会在存在一个对象时返回一个对象。


非常好用!我几乎确定在CoreData中应该有一个简单的解决方案,只是我不知道而已 =)谢谢,我的应用程序的下一个版本将使用existingObjectWithID:error: =) - Petro Korienev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接