1

我有一个应用程序,它首先将一些数据加载到 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;
}
4

1 回答 1

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

除了将背景 MOC 设置为 mainMOC 之外,您还可以将其设置为 parentContext。加载然后保存到其中会将更改放在主 MOC 的“上方”。主 MOC 将在下次执行 fetch 操作时看到这些更改(注意 NSFetchRequest 的属性)。

注意:有些人报告说(它也在 Erica Sadun 的书中作为注释出现),在第一次 saveToURL 之后,您需要关闭,然后打开以使一切正常。

编辑

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

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

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

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

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

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

我有约会,所以现在必须停下来。但是,解决这些问题,接下来就是

于 2012-05-03T12:19:07.403 回答