保存UIManagedDocument时应用程序崩溃

时间:2012-05-03 10:42:30

标签: objective-c ios core-data save uimanageddocument

我有一个应用程序首先将一些数据加载到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,以显示数据库包含的不同实体。

现在,在helper类中,这是通过每个视图的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 个答案:

答案 0 :(得分:4)

你并没有真正提供太多代码。你给出的唯一真实线索是你使用多个线程。

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
        }
    }];
}

您可以将其父项提交给parentContext,而不是将您的后台MOC提供给mainMOC。加载然后保存到它将把更改置于主MOC“上方”。主MOC将在下次执行获取操作时看到这些更改(请注意NSFetchRequest的属性)。

注意:有些人报告过(在Erica Sadun的书中也有说明),在第一次saveToURL之后,你需要关闭,然后打开以使一切正常。

修改

这真的很长。如果你有更多积分,我建议聊聊。实际上,我们不能通过SO来实现,但我们可以通过其他媒介来实现。我会尽量简短,但请回去重读我发布的内容,并小心注意,因为您的代码仍然违反了几个租户。

首先,在viewDidLoad()中,您直接将文档分配给调用openDatabaseAndUseBlock的结果。当时文档未处于可用状态。在完成处理程序触发之前,您不希望文档可访问,这在openDatabaseAndUseBlock()返回之前不会发生。

其次,只在您第一次创建数据库时调用saveToURL(在openDatabaseAndUseBlock()内)。不要在其他任何地方使用它。

第三。注册通知中心以接收所有事件(只记录它们)。这将极大地帮助您进行调试,因为您可以看到正在发生的事情。

第四,子类UIManagedDocument,并覆盖handleError,看看它是否被调用...这是唯一的方法,如果/当它发生时你会看到确切的NSError。

3/4主要是帮助您调试,而不是生产代码所必需的。

我有预约,所以现在必须停下来。但是,请解决这些问题,并在此处