ios - 奇怪的 NSManagedObject 行为

标签 ios objective-c core-data

我遇到了奇怪的 CoreData 问题。
首先,在我的项目中我使用了很多框架,所以有很多问题来源 - 所以我考虑创建最小的项目来重复我的问题。你可以克隆 Test project on Github并逐步重复我的测试。
所以,问题:
NSManagedObject 绑定(bind)到它的 NSManagedObjectID,它不允许从 NSManagedObjectContext 中正确删除对象
因此,重现步骤:
在我的 AppDelegate 中,我像往常一样设置 CoreData 堆栈。 AppDelegate 具有 managedObjectContext 属性,可以访问该属性以获取主线程的 NSManagedObjectContext。应用程序的对象图由一个实体 Messagebodyfromtimestamp 属性组成。 应用程序只有一个 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 的 message (null)
所以,它显示:
1)消息实际上在数据库中不存在。无法获取。
2) 第一个版本的代码以某种方式在上下文中保留了已删除的 message 对象(可能是因为它的对象 ID 被保留用于 block 调用)。
但是为什么我可以通过它的 id 获取已删除的对象?我需要知道。
显然,首先,我将 objectId 更改为 __weak。甚至在 block 之前就崩溃了:)
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-copy 的实现类似于NSManagedObjectID
上的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: 实现:

#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 变量,所以在 block 调用时它可以被释放,所以我们必须检查它是否是有效对象。所以我们在 block 之前保存它的 class (它不是保留,它是分配)并通过 safePointer:isMemberOfClass:
在 block 中检查它 所以现在,通过它的 managedObjectId 重新获取对象对我来说是UNTRUSTED模式。
有人对我在这种情况下应该怎么做有什么建议吗?要使用 __unsafe_unretained 并检查?但是,这个 managedObjectId 也可以被另一个代码保留,所以它会导致 could not fulfill 属性访问崩溃。或者每次都通过谓词获取对象? (如果对象由 3-4 个属性唯一定义怎么办?将它们全部保留用于完成 block ?)。异步处理托管对象的最佳模式是什么?
很抱歉长时间的研究,提前致谢。

附言您仍然可以重复我的步骤或使用 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... 将仅在对象存在时返回对象。

关于ios - 奇怪的 NSManagedObject 行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19061132/

相关文章:

ios - 如果蓝牙关闭 iOS,禁用警告对话框

ios - 从命令行运行 Apple Instruments 会抛出 : The simulated application quit

ios - iOS : Use of undeclared identifier error in simple calculator [closed]

ios - 我无法在 MagicalRecord 中找到 FirstByAttribute

ios - 为什么 NSManagedObject 实例不应该从一个线程传递到另一个线程?

ios - 设计表单布局的最佳方法是什么

ios - 在 GCD 中,有没有办法判断当前队列是否并发?

iphone - 如何访问默认的 iOS 声音以将其设置为通知声音?

ios - 使用 UIGesturerecognizer 或其他方式移动时如何知道 subview 是否相交?

ios - 当 App 退出并重新启动时,NSManagedObjectContext 不会保存