Сбой приложения при сохранении UIManagedDocument

У меня есть приложение, которое сначала загружает некоторые данные в UIManagedDocument, а затем выполняет saveToURL:forSaveOperation:completionHandler:. Внутри блока completeHandler он обновляет различные элементы этой базы данных, а когда это сделано, он делает еще одно сохранение.

Кроме того, в приложении есть 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;
}

person David    schedule 03.05.2012    source источник


Ответы (1)


Вы действительно не предоставили много кода. Единственная реальная подсказка, которую вы дали, заключалась в том, что вы используете несколько потоков.

UIManagedDocument имеет два ManagedObjectContexts (один указан для основной очереди, а другой — для частной очереди), но доступ к каждому из них по-прежнему должен осуществляться только из их собственного потока.

Таким образом, вы должны использовать manageDocument.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. Таким образом, если вы знаете, что ваш код выполняется в основном потоке (как и все ваши обратные вызовы пользовательского интерфейса), вам не нужно использовать 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 увидит эти изменения при следующем выполнении операции выборки (обратите внимание на свойства NSFetchRequest).

ПРИМЕЧАНИЕ. Некоторые люди сообщают (и это также появляется как примечание в книге Эрики Садун), что после самого первого saveToURL вам нужно закрыть, а затем открыть, чтобы все заработало правильно.

ИЗМЕНИТЬ

Это становится очень длинным. Если бы у вас было больше очков, я бы предложил пообщаться. На самом деле мы не можем сделать это через SO, но мы можем сделать это через другой носитель. Я постараюсь быть кратким, но, пожалуйста, вернитесь и перечитайте то, что я опубликовал, и обратите особое внимание, потому что ваш код все еще нарушает несколько арендаторов.

Во-первых, в viewDidLoad() вы напрямую связываете свой документ с результатом вызова openDatabaseAndUseBlock. В это время документ не находится в пригодном для использования состоянии. Вы не хотите, чтобы документ был доступен до тех пор, пока не сработают обработчики завершения, что не произойдет до возврата openDatabaseAndUseBlock().

Во-вторых, вызывайте saveToURL только при первом создании базы данных (внутри openDatabaseAndUseBlock()). Не используйте его в другом месте.

В третьих. Зарегистрируйтесь в центре уведомлений, чтобы получать все события (просто записывайте их). Это очень поможет вам в отладке, потому что вы можете видеть, что происходит.

В-четвертых, создайте подкласс UIManagedDocument и переопределите handleError и посмотрите, вызывается ли он... это единственный способ увидеть точную ошибку NSError, если/когда это произойдет.

3/4 предназначены в основном для помощи в отладке, а не для вашего производственного кода.

У меня назначена встреча, поэтому я должен остановиться сейчас. Тем не менее, решить эти проблемы, и вот на

person Jody Hagins    schedule 03.05.2012
comment
Спасибо за ответ, Джоди. Я только что отредактировал сообщение, чтобы включить код для лучшего понимания. Поменял все сохранения, кроме первого с updateChangeCount но вроде не исправляет зависание приложения при нажатии кнопки (почти всегда кнопка Удалить). Кроме того, он случайным образом говорит, что нет элемента для удаления, когда он есть (вероятно, из-за того, что база данных еще не сохранена, я уверен, что запрос на поиск элемента правильный). - person David; 04.05.2012
comment
Вы все еще не совсем правы. К сожалению, у меня должны быть друзья на ужин в 7:00, сейчас 6:43, а я в 45 минутах от дома. У меня также есть ощущение, что происходит нечто большее, чем намекают эти фрагменты кода. Если вы можете опубликовать немного больше о том, что вы делаете, я посмотрю, смогу ли я собрать пример UIManagedDocument, который похож на то, что вы делаете сегодня поздно вечером. - person Jody Hagins; 05.05.2012
comment
Еще раз спасибо, Джоди! Я снова отредактировал сообщение, чтобы показать больше кода. Надеюсь, это поможет вам сделать лучшую идею! Я нахожу свой код очень похожим на то, что вы написали. Я пытался встроить содержимое loadDataIntoDatabase: в dispatch_async(dispatch_get_main_queue(), ^{});, но это ничего не меняет. Может быть, я плохо подхожу ко всему этому... Я просто хочу, чтобы база данных заполнялась данными один раз, а затем обновляла некоторые параметры при нажатии кнопки, считывая их из другого файла, который время от времени модифицируется. Думаю, не должно быть так сложно... :S - person David; 05.05.2012
comment
Спасибо за помощь, Джоди. Я пытаюсь решить эту проблему, но, к сожалению, безуспешно :( Как вы думаете, мы могли бы поговорить в каком-то чате или что-то в этом роде? Я был бы очень признателен, потому что я думаю, что я довольно потерян... - person David; 07.05.2012
comment
@JodyHagins Если нужно вызывать saveToURL только при создании, то какой смысл в UIDocumentSaveForOverwriting? - person Brad Thomas; 12.12.2016