我遇到了奇怪的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;
}
});
}
好吧,我检测到应用程序崩溃,所以我尝试以另一种方式获取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
。甚至在阻止之前崩溃了:)
所以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
的{{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
答案 0 :(得分:2)
请勿使用objectWithID:
。使用existingObjectWithID:error:
。根据文档the former:
...总是返回一个对象。持久性存储中的数据 假设存在objectID表示的 - 如果不存在,则表示存在 当您访问任何属性时,返回的对象会抛出异常( 是,当故障发生时)。这种行为的好处是它 允许您创建和使用故障,然后创建基础数据 稍后或在单独的背景下。
这正是你所看到的。你得到一个对象,因为Core Data认为你必须要一个具有该ID的对象,即使它没有。当你试图存储它时,没有在过渡期间创建一个实际的对象,它不知道该做什么,你得到了例外。
existingObject...
只有在存在对象时才会返回该对象。