由于 NSObjectInaccessible 异常,无法更新 NSManagedObject
Posted
技术标签:
【中文标题】由于 NSObjectInaccessible 异常,无法更新 NSManagedObject【英文标题】:Cannot update NSManagedObject because of NSObjectInaccessible exception 【发布时间】:2013-04-24 18:29:00 【问题描述】:我正在构建一个用于显示资产(PDF、视频等)的应用程序。
首先下载一个 JSON 并将其解析为核心数据对象
这些对象是一组层次结构的节点,它们在我的模型中设置了关系。每个节点可以是 FILE 或 FOLDER。
然后我在我的 NSManagedObject 子类中内置了实例方法,这些方法将下载与该对象关联的文件(即 PDF)。然后设置
self.isAvailable = [NSNumber numberWithBool:YES];
同时,我有一个显示资产列表的 UITableView。最终它将实时更新,但现在这是我遇到问题的地方。我首先让视图控制器保留一个指向 CoreData 对象的指针,该对象代表它显示的文件夹,但似乎如果上下文得到更新,则指针变得无效(即故障失败)。
核心数据并没有非常具体地说明问题是什么,甚至问题发生在哪里,但是当我设置 isAvailable 时它似乎崩溃了
*** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x1d5f9e50 <x-coredata://EDE66B97-B142-4E87-B445-76CAB965B676/Node/p58>''
我觉得问题在于我不应该只保留对核心数据对象的强引用作为我的模型。有没有更好(不那么崩溃)的方法来做到这一点?
我已经开始使用 NSFetchedResultsController 并改用 objectID,但我还没有得到任何结果。
- (void)populateChildren
NSString * urlString = [NSString stringWithFormat:@"%@/%@", [CMPConstants hostURLString], self.SBUCode];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest * request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:self.downloadQueue completionHandler:^(NSURLResponse * response, NSData * data, NSError * error)
if (data)
NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
[self processParsedObject:dict];
else
NSLog(@"%@", urlString);
];
#pragma mark - Parse JSON into NSManagedObjects
- (void)processParsedObject:(id)object
[self processParsedObject:object depth:0 parent:nil key:nil];
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
- (void)processParsedObject:(id)object depth:(int)depth parent:(Node *)parent key:(NSString*)key
if ([object isKindOfClass:[NSDictionary class]])
if (depth == 0)
// Grab content node if depth is 0;
object = [object valueForKey:@"content"];
// FIXME: Change this to a real primary key once we get one.
static NSString * primaryKey = @"name";
// Look for existing object
Node * testNode = [Node MR_findFirstByAttribute:primaryKey withValue:[object valueForKey:primaryKey]];
// Create new node pointer
Node * newNode;
if (testNode)
// Update existing Node
newNode = testNode;
else
// Build a new Node Object
newNode = [Node MR_createEntity];
newNode.isAvailable = [NSNumber numberWithBool:NO];
// Get keys
NSArray * keys = @[@"name",
@"type",
@"index",
@"size",
@"videoDemensions",
@"videoId",
@"fileName",
@"fileType",
@"path"];
if ([[object valueForKey:@"type"] isEqual:[NSNull null]])
NSLog(@"%@", object);
// Loop to set value for keys.
for (NSString * key in keys)
id value = [object valueForKey:key];
if (![[object valueForKey:key] isKindOfClass:[NSNull class]])
[newNode setValue:value forKey:key];
// Set calculated properties.
[newNode setSbu:[self SBUCode]];
[newNode setParent:parent];
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
// Sync local file.
if (!newNode.isAvailable.boolValue)
[newNode aquireFileInQueue:self.downloadQueue];
// Process children
for(NSString * newKey in [object allKeys])
id child = [object objectForKey:newKey];
[self processParsedObject:child depth:depth+1 parent:newNode key:newKey];
else if ([object isKindOfClass:[NSArray class]])
for(id child in object)
[self processParsedObject:child depth:depth+1 parent:parent key:nil];
else
// Nothing here, this processes each field.
这个方法是Node类的一个实例方法。
- (void)aquireFileInQueue:(NSOperationQueue *)queue
if ([self.type isEqualToString:@"VIDEO"])
// Videos are available, but not downloaded.
self.isAvailableValue = YES;
return;
if (self.path == nil || self.fileName == nil)
NSLog(@"Path or Filename for %@ was nil", self.name);
return;
// Build the download URL !! MAKE SURE TO ADD PERCENT ESCAPES, this will protect against spaces in the file name
// Also make sure to slash-separate the path and fileName
NSURL * downloadURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@",
[self.path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
[self.fileName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
// Build the download request
NSURLRequest * downloadRequest = [NSURLRequest requestWithURL:downloadURL];
// FIXME: Authentication Code for JSON service
// Show network activity indicator
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
// Send Asynchronus Request for fileData
[NSURLConnection sendAsynchronousRequest:(NSURLRequest *)downloadRequest queue:queue completionHandler:^(NSURLResponse * response, NSData * data, NSError * error)
// Hide network activity indicatior
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
// Cast URL Response to HTTPURLResponse
NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)response;
// If statusCode is 200 (successful) and data is not nil, save data
if (httpResponse.statusCode == 200 && data)
[data writeToURL:[self fileURL] atomically:NO];
[[NSOperationQueue mainQueue] addOperationWithBlock:^
[self setIsAvailable:[NSNumber numberWithBool:YES]];
];
];
- (void)prepareForDeletion
// Remove file from Filesystem
[[NSFileManager defaultManager] removeItemAtURL:[self fileURL] error:nil];
- (NSURL *)fileURL
// Return local file URL
return [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/%@", [Node applicationDocumentsDirectory], self.fileName]];
【问题讨论】:
上下文是如何更新的?您是否正在合并来自 did-save 通知的更改?还有什么? 听起来像是线程问题?您是否跨线程共享 NSManagedObject?如果是这样,那么根据 Apple 的说法,这是一个禁忌。您应该使用objectID
,就像您开始使用一样。 NSFetchedResultsController 是一个很好的方法,如果你想确保你的表视图在不同的上下文/线程中发生的变化保持最新。
NSManagedObject 具体类有一个名为 -(void)aquireFile 的方法。此方法将文件下载到单独的队列中,然后分派回 mainQueue 以设置 isAvailable。我的理解是这不应该导致任何队列或上下文问题。我会错过什么吗?
我添加了一些代码。对不起,它不像我最初写的那么优雅,因为我正在尝试追踪这个错误。
【参考方案1】:
我不熟悉 MagicalRecords
当上下文持有未出错的对象(对象存根)但数据库中的实际对象不存在(已删除或从未保存)时,会发生“无法完成故障”错误。
我的第一个建议:
如果您在多线程环境中工作,请尝试保存有故障的对象。
使用 -existingObjectWithId:error:
并通过以下方式获取请求:
[fetchRequest setReturnsObjectsAsFaults:NO];
[fetchRequest setIncludesPropertyValues:YES];
[fetchRequest setRelationshipKeyPathsForPrefetching:/*relationships you can afford to prefetch*/];
我的第二条建议(调试您的问题):
在您每次保存到商店之前打印您的deletedObjects
设置,以查看导致错误的上下文。
我的第三条建议: 合并对主要上下文的更改(我的猜测是 MagicalRecords 会为您执行此操作)。
注意 1:可能会隐含删除(例如,您不会通过在级联/拒绝模式下设置关系来明确使用
deleteObject:
)注意 2:在多线程环境 (AFAIK) 中,您无法避免此异常,除非您通过主上下文(使用
parentContext
)或始终使用预取对象(不直接使用关系)传递所有保存。
【讨论】:
我一头雾水,既然-(void)aquireFile是Node对象的方法,怎么可能不存在呢?在错误起作用之前,我需要做些什么来将对象“提交”到上下文中吗? 我的对象肯定被正确放入上下文中,如果我在 sqlite3 中打开 .sqlite 文件,我可以看到我的所有记录。 我会测试一些东西并回复你。同时,我注意到您在下载完成块的主线程中修改了属于 BG 上下文的对象。我建议您更改事物的顺序,发出下载并完成后,在 BG 上下文中重新获取对象,如果存在,请更新其状态。 我想通了,我的初始下载应该在 mainQueue 上执行 processParsedObject 而不是 downloadQueue。谢谢你指出这一点。以上是关于由于 NSObjectInaccessible 异常,无法更新 NSManagedObject的主要内容,如果未能解决你的问题,请参考以下文章