我遇到了奇怪的CoreData问题。
首先,我的项目使用了很多框架,所以问题的来源很多 - 因此我考虑创建一个重复我的问题的最小项目。您可以克隆Github上的测试项目并逐步重复我的测试。
因此,问题如下:
NSManagedObject绑定了它的NSManagedObjectID,这使得对象无法从NSManagedObjectContext中正确删除
因此,重现步骤如下:
在我的AppDelegate中,我像往常一样设置了CoreData堆栈。AppDelegate有一个
简要解释 - 我们使用
因此,目前,按其managedObjectId重新获取对象是我不信任的模式。
有人有什么建议,我应该如何处理这种情况?继续使用__unsafe_unretained并检查吗?但是,这个managedObjectId也可以被另一个代码保留,所以它将导致在属性访问时
抱歉进行了长时间的研究,提前致谢。
P.S. 您仍然可以重复我的步骤或使用测试项目进行自己的实验。
首先,我的项目使用了很多框架,所以问题的来源很多 - 因此我考虑创建一个重复我的问题的最小项目。您可以克隆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打印了
因此,它显示:
1)消息实际上不存在于DB中。 它无法获取。
2)代码的第一个版本在某种程度上保留了已删除的message
对象(可能是因为其对象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
上次尝试使用__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. 您仍然可以重复我的步骤或使用测试项目进行自己的实验。