我遇到了奇怪的 CoreData 问题。
首先,在我的项目中,我使用了很多框架,所以有很多问题来源 - 所以我考虑创建重复我的问题的最小项目。您可以在 Github 上克隆测试项目并逐步重复我的测试。
所以,问题:
NSManagedObject 与它的 NSManagedObjectID 相关联,它不允许从 NSManagedObjectContext 正确删除对象
所以,重现步骤:
在我的 AppDelegate 中,我像往常一样设置 CoreData 堆栈。AppDelegate 有managedObjectContext
属性,可以访问该属性来获取主线程的 NSManagedObjectContext。应用程序的对象图由一个Message
带有body
, from
,的实体组成timestamp
属性。应用程序只有一个 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'edmessage (null)
所以,它显示:
1)消息实际上在数据库中不存在。无法获取。
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
's-copy
的实现方式与return self
Last NSManagedObjectID
try is __unsafe_unretained
for 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;
}
...
安全对象: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
变量,所以在块调用时它可以被释放,所以我们必须检查它是否是有效的对象。所以我们将它保存class
在块之前(它不是保留,它是分配)并通过它在块中检查它safePointer:isMemberOfClass:
所以现在,通过它的 managedObjectId 重新获取对象对我来说是UNTRUSTED模式。
有人对我在这种情况下应该怎么做有任何建议吗?要使用 __unsafe_unretained 并检查?但是,这个 managedObjectId 也可以被另一个代码保留,所以它会导致could not fulfill
属性访问崩溃。或者每次都通过谓词获取对象?(如果对象是由 3-4 个属性唯一定义的怎么办?保留它们以完成块?)。异步处理托管对象的最佳模式是什么?
对不起,长期研究,提前谢谢。PS您仍然可以重复我的步骤或使用测试项目
进行自己的实验