保存 UIManagedDocument 时应用程序崩溃
Posted
技术标签:
【中文标题】保存 UIManagedDocument 时应用程序崩溃【英文标题】:App crashes when saving UIManagedDocument 【发布时间】:2012-05-03 10:42:29 【问题描述】:我有一个应用程序,它首先将一些数据加载到 UIManagedDocument 中,然后执行 saveToURL:forSaveOperation:completionHandler:
。在completionHandler 块中,它会更新这个数据库的各种元素,当它完成时,它会再次保存。
除此之外,该应用程序还有 3 个按钮,分别用于重新加载数据、重新更新数据和删除数据库的一个实体。在每个按钮方法中,最后一条指令也是保存。
当我在模拟器中运行所有这些时,一切都很顺利。但在设备中没有。它不断崩溃。我观察到,通常情况下,当按下“删除”按钮,或者重新加载或重新更新数据库时,它会崩溃。它总是在saveToURL
操作中。
在我看来,当有多个线程保存数据库时,问题就出现了。由于设备执行代码的速度较慢,可能会同时节省多个成本,而应用程序无法正确处理它们。此外,有时删除按钮不会删除实体,并说不存在(当它存在时)。
我对此完全感到困惑,所有这些保存操作都必须完成......事实上,如果我删除它们,应用程序的行为会更加不连贯。
有什么建议可以解决这个问题吗?非常感谢你!
[编辑] 在这里我发布有问题的代码。对于第一次加载数据,我使用了一个辅助类,特别是这两种方法:
+ (void)loadDataIntoDatabase:(UIManagedDocument *)database
[database.managedObjectContext performBlock:^
// Read from de plist file and fill the database
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success)
[DataHelper completeDataOfDatabase:database];
];
+ (void)completeDataOfDatabase:(UIManagedDocument *)database
[database.managedObjectContext performBlock:^
// Read from another plist file and update some parameters of the already existent data (uses NSFetchRequest and works well)
// [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
[database updateChangeCount:UIDocumentChangeDone];
];
在视图中,我有 3 个操作方法,如下所示:
- (IBAction)deleteButton
[self.database.managedObjectContext performBlock:^
NSManagedObject *results = ;// The item to delete
[self.database.managedObjectContext deleteObject:results];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
];
- (IBAction)reloadExtraDataButton
[DataHelper loadDataIntoDatabase:self.database];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
- (IBAction)refreshDataButton
[DataHelper completeDataOfDatabase:self.database];
//[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
[编辑2]更多代码:首先初始视图是这样执行viewDidLoad的:
- (void)viewDidLoad
[super viewDidLoad];
self.database = [DataHelper openDatabaseAndUseBlock:^
[self setupFetchedResultsController];
];
这就是 setupFetchedResultsController 方法的样子:
- (void)setupFetchedResultsController
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Some entity name"];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.database.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
应用程序的每个视图(它有选项卡)都有一个不同的 setupFetchedResultsController 以显示数据库包含的不同实体。
现在,在辅助类中,这是第一个通过每个视图的 viewDidLoad 执行的类方法:
+ (UIManagedDocument *)openDatabaseAndUseBlock:(completion_block_t)completionBlock
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:@"Database"];
UIManagedDocument *database = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[database.fileURL path]])
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success)
[self loadDataIntoDatabase:database];
completionBlock();
];
else if (database.documentState == UIDocumentStateClosed)
// Existe, pero cerrado -> Abrir
[database openWithCompletionHandler:^(BOOL success)
[self loadDataIntoDatabase:database];
completionBlock();
];
else if (database.documentState == UIDocumentStateNormal)
[self loadDataIntoDatabase:database];
completionBlock();
return database;
【问题讨论】:
【参考方案1】:您并没有真正提供太多代码。您提供的唯一真正线索是您正在使用多个线程。
UIManagedDocument 有两个 ManagedObjectContexts(一个为主队列指定,另一个为私有队列指定),但它们仍然只能在各自的线程中访问。
因此,您只能在主线程中使用 managedDocument.managedObjectContext。如果要从另一个线程使用它,则必须使用 performBlock 或 performBlockAndWait。同样,您永远无法知道您是在父上下文的私有线程上运行的,因此如果您想专门针对父上下文执行某些操作,则必须使用 performBlock*。
最后,你真的不应该调用 saveToURL,除非你最初创建数据库。 UIManagedDocument 将自动保存(在它自己的时间)。
如果你想鼓励它提前保存,你可以发送 updateChangeCount: UIDocumentChangeDone 告诉它有更改需要保存。
编辑
您应该只在第一次创建文件时调用 saveToURL。使用 UIManagedDocument,无需再次调用它(它实际上会导致一些意想不到的问题)。
基本上,当您创建文档时,在完成处理程序执行之前不要设置您的 iVar。否则,您可能正在使用处于部分状态的文档。在这种情况下,请在完成处理程序中使用这样的帮助程序。
- (void)_document:(UIManagedDocument*)doc canBeUsed:(BOOL)canBeUsed
dispatch_async(dispatch_get_main_queue(), ^
if (canBeUsed)
_document = doc;
// Now, the document is ready.
// Fire off a notification, or notify a delegate, and do whatever you
// want... you really should not use the document until it's ready, but
// as long as you leave it nil until it is ready any access will
// just correctly do nothing.
else
_document = nil;
// Do whatever you want if the document can not be used.
// Unfortunately, there is no way to get the actual error unless
// you subclass UIManagedDocument and override handleError
];
并且要初始化您的文档,例如...
- (id)initializeDocumentWithFileURL:(NSURL *)url
if (!url)
url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:@"Default_Project_Database"];
UIManagedDocument *doc = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[doc.fileURL path]])
// The file does not exist, so we need to create it at the proper URL
[doc saveToURL:doc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success)
[self _document:doc canBeUsed:success];
];
else if (doc.documentState == UIDocumentStateClosed)
[doc openWithCompletionHandler:^(BOOL success)
[self _document:doc canBeUsed:success];
];
else
// You only need this if you allow a UIManagedDocument to be passed
// in to this object -- in which case the code above that initializes
// the <doc> variable will be conditional on what was passed...
BOOL success = doc.documentState == UIDocumentStateNormal;
[self _document:doc canBeUsed:success];
上面的“模式”是必要的,以确保您在文档完全可以使用之前不使用它。现在,那段代码应该是您调用 saveToURL 的唯一时间。
请注意,根据定义,document.managedObjectContext 的类型为 NSMainQueueConcurrencyType。因此,如果你知道你的代码在主线程上运行(就像你所有的 UI 回调一样),你就不必使用 performBlock。
但是,如果您实际上是在后台进行加载,请考虑..
- (void)backgroundLoadDataIntoDocument:(UIManagedDocument*)document
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.parentContext = document.managedObjectContext;
[moc performBlock:^
// Do your loading in here, and shove everything into the local MOC.
// If you are loading a lot of stuff from the 'net (or elsewhere),
// consider doing it in strides, so you deliver objects to the document
// a little at a time instead of all at the end.
// When ready to save, call save on this MOC. It will shove the data up
// into the MOC of the document.
NSrror *error = nil;
if ([moc save:&error])
// Probably don't have to synchronize calling updateChangeCount, but I do it anyway...
[document.managedObjectContext performBlockAndWait:^
[document updateChangeCount:UIDocumentChangeDone];
];
else
// Handle error
];
除了将背景 MOC 设置为 mainMOC 之外,您还可以将其设置为 parentContext。加载然后保存到其中会将更改放在主 MOC 的“上方”。主 MOC 将在下次执行 fetch 操作时看到这些更改(注意 NSFetchRequest 的属性)。
注意:有些人报告(这也出现在 Erica Sadun 的书中作为注释),在第一次 saveToURL 之后,您需要关闭,然后再打开以使一切正常。
编辑
这真的很长。如果你有更多的积分,我建议你聊天。实际上,我们不能通过 SO 来实现,但我们可以通过另一种媒介来实现。我会尽量简短,但请返回并重新阅读我发布的内容,并仔细注意,因为您的代码仍然违反了几个租户。
首先,在 viewDidLoad() 中,您直接将文档分配给调用 openDatabaseAndUseBlock 的结果。那时文档不处于可用状态。在完成处理程序触发之前,您不希望文档可以访问,这在 openDatabaseAndUseBlock() 返回之前不会发生。
其次,仅在您第一次创建数据库时调用 saveToURL(在 openDatabaseAndUseBlock() 中)。不要在其他任何地方使用它。
第三。在通知中心注册以接收所有事件(只需记录它们)。这将极大地帮助您进行调试,因为您可以看到正在发生的事情。
第四,子类化 UIManagedDocument,并覆盖 handleError,并查看它是否被调用...这是您看到确切的 NSError(如果/当它发生时)的唯一方法。
3/4 主要是帮助你调试,不是你的生产代码所必需的。
我有一个约会,所以现在必须停下来。但是,解决这些问题,就可以了
【讨论】:
感谢乔迪的回复。我刚刚编辑了帖子以包含代码,以便更好地理解。我改变了所有的储蓄,除了updateChangeCount
的第一个储蓄,但它似乎并没有解决按下按钮时应用程序冻结的问题(几乎总是删除按钮)。另外,它随机说没有要删除的项目(可能是因为尚未保存数据库,我确定查找项目的请求是正确的)。
您仍然没有完全正确。不幸的是,我应该在 7:00 约朋友过来吃晚饭,现在是 6:43,我离家 45 分钟。我也有一种感觉,除了 sn-ps 所暗示的代码之外,还有更多的事情发生。如果您可以发布更多关于您正在做什么的信息,我会看看我是否可以整理一个 UIManagedDocument 示例,该示例类似于您今晚晚些时候正在做的事情。
再次感谢乔迪!我再次编辑了帖子以显示更多代码。希望它可以帮助您做出更好的主意!我发现我的代码与您发布的代码非常相似。我试图将loadDataIntoDatabase:
的内容嵌入到dispatch_async(dispatch_get_main_queue(), ^);
中,但它似乎并没有改变任何东西。也许我正在以一种糟糕的方式处理这一切......我只想让数据库填充一次数据,然后在按下按钮时更新一些参数,方法是从另一个经常修改的文件中读取它们。不应该这么复杂,我想... :S
感谢您的帮助,乔迪。我正在尝试解决这个问题,但没有成功,不幸的是:(你认为我们可以聊聊什么吗?我会非常感激,因为我觉得我很迷茫......
@JodyHagins 如果只在创建时调用 saveToURL,那么 UIDocumentSaveForOverwriting 的意义何在?以上是关于保存 UIManagedDocument 时应用程序崩溃的主要内容,如果未能解决你的问题,请参考以下文章
自动保存不适用于 UIManagedDocument 上的 NSUndoManager