Implementando Importação de Dados Principais Rápida e Eficiente no iOS 5

101

Pergunta : Como faço para que meu contexto filho veja as alterações persistentes no contexto pai para que acionem meu NSFetchedResultsController para atualizar a IU?

Aqui está a configuração:

Você tem um aplicativo que baixa e adiciona muitos dados XML (cerca de 2 milhões de registros, cada um com aproximadamente o tamanho de um parágrafo normal de texto). O arquivo .sqlite passa a ter cerca de 500 MB. Adicionar esse conteúdo ao Core Data leva tempo, mas você deseja que o usuário seja capaz de usar o aplicativo enquanto os dados são carregados no armazenamento de dados de forma incremental. Deve ser invisível e imperceptível para o usuário que grandes quantidades de dados estão sendo movidas, então sem travamentos, sem tremores: rola como manteiga. Ainda assim, o aplicativo é mais útil quanto mais dados são adicionados a ele, portanto, não podemos esperar para sempre que os dados sejam adicionados ao armazenamento de dados principais. No código, isso significa que eu realmente gostaria de evitar um código como este no código de importação:

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];

O aplicativo é iOS 5 apenas, então o dispositivo mais lento que ele precisa para suportar é um iPhone 3GS.

Aqui estão os recursos que usei até agora para desenvolver minha solução atual:

Guia de programação de dados principais da Apple: importando dados com eficiência

  • Use Autorelease Pools para manter a memória baixa
  • Custo de relacionamento. Importe planos e, em seguida, remende os relacionamentos no final
  • Não pergunte se você pode ajudar, isso retarda as coisas de uma maneira O (n ^ 2)
  • Importar em lotes: salvar, redefinir, drenar e repetir
  • Desligue o Undo Manager na importação

iDeveloper TV - Core Data Performance

  • Use 3 contextos: tipos de contexto mestre, principal e de confinamento

iDeveloper TV - Atualização de dados principais para Mac, iPhone e iPad

  • Executar salvamentos em outras filas com performBlock torna as coisas mais rápidas.
  • A criptografia torna as coisas mais lentas, desligue-a se puder.

Importando e exibindo grandes conjuntos de dados em dados centrais por Marcus Zarra

  • Você pode desacelerar a importação dando tempo ao loop de execução atual, para que as coisas pareçam tranquilas para o usuário.
  • O código de amostra prova que é possível fazer grandes importações e manter a IU responsiva, mas não tão rápido quanto com 3 contextos e salvamento assíncrono em disco.

Minha Solução Atual

Eu tenho 3 instâncias de NSManagedObjectContext:

masterManagedObjectContext - Este é o contexto que possui o NSPersistentStoreCoordinator e é responsável por salvar em disco. Faço isso para que meus salvamentos possam ser assíncronos e, portanto, muito rápidos. Eu crio no lançamento assim:

masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];

mainManagedObjectContext - Este é o contexto que a IU usa em todos os lugares. É um filho do masterManagedObjectContext. Eu crio assim:

mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];

backgroundContext - Este contexto é criado em minha subclasse NSOperation que é responsável por importar os dados XML para Core Data. Eu o crio no método principal da operação e o vinculo ao contexto mestre lá.

backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];

Isso realmente funciona muito, muito rápido. Apenas fazendo essa configuração de 3 contextos, consegui melhorar minha velocidade de importação em mais de 10 vezes! Honestamente, isso é difícil de acreditar. (Este design básico deve fazer parte do modelo padrão de dados principais ...)

Durante o processo de importação, salvo 2 formas diferentes. A cada 1000 itens que salvo no contexto de fundo:

BOOL saveSuccess = [backgroundContext save:&error];

Em seguida, no final do processo de importação, salvo no contexto principal / pai que, aparentemente, envia modificações para os outros contextos filho, incluindo o contexto principal:

[masterManagedObjectContext performBlock:^{
   NSError *parentContextError = nil;
   BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];

Problema : o problema é que minha IU não será atualizada até que eu recarregue a visualização.

Eu tenho um UIViewController simples com um UITableView que está sendo alimentado com dados usando um NSFetchedResultsController. Quando o processo de importação é concluído, o NSFetchedResultsController não vê nenhuma mudança no contexto pai / mestre e, portanto, a IU não é atualizada automaticamente como estou acostumado a ver. Se eu retirar o UIViewController da pilha e carregá-lo novamente, todos os dados estarão lá.

Pergunta : Como faço para que meu contexto filho veja as alterações persistentes no contexto pai para que acionem meu NSFetchedResultsController para atualizar a IU?

Eu tentei o seguinte, que apenas trava o aplicativo:

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    NSError *error = nil;
    BOOL saveSuccess = [masterManagedObjectContext save:&error];

    [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}

- (void)contextChanged:(NSNotification*)notification
{
    if ([notification object] == mainManagedObjectContext) return;

    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(contextChanged:) withObject:notification waitUntilDone:YES];
        return;
    }

    [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
David Weiss
fonte
26
+1000000 para a pergunta mais bem formada e preparada de todos os tempos. Eu também tenho uma resposta ... Levarei alguns minutos para digitá-la ...
Jody Hagins
1
Quando você diz que o aplicativo está travado, onde ele está? O que está fazendo?
Jody Hagins
Desculpe trazer isso à tona depois de muito tempo. Você pode esclarecer o que significa "Importar plano e corrigir relacionamentos no final"? Você ainda não precisa ter esses objetos na memória para estabelecer relacionamentos? Estou tentando implementar uma solução muito semelhante à sua e poderia realmente usar alguma ajuda para reduzir o consumo de memória.
Andrea Sprega
Veja o Apple Docs com link no primeiro deste artigo. Isso explica isso. Boa sorte!
David Weiss
1
Uma pergunta muito boa e eu
aprendi

Respostas:

47

Você provavelmente também deve salvar o MOC mestre em passos largos. Não faz sentido ter aquele MOC esperando até o fim para salvar. Ele tem seu próprio thread e também ajudará a manter a memória baixa.

Você escreveu:

Em seguida, no final do processo de importação, salvo no contexto principal / pai que, aparentemente, envia modificações para os outros contextos filho, incluindo o contexto principal:

Em sua configuração, você tem dois filhos (o MOC principal e o MOC de fundo), ambos sendo pais do "mestre".

Quando você salva em uma criança, isso empurra as mudanças para o pai. Outros filhos desse MOC verão os dados na próxima vez que executarem uma busca ... eles não são notificados explicitamente.

Portanto, quando o BG salva, seus dados são enviados para MASTER. Observe, no entanto, que nenhum desses dados está no disco até que o MASTER seja salvo. Além disso, quaisquer novos itens não obterão IDs permanentes até que o MASTER salve no disco.

Em seu cenário, você está puxando os dados para o MAIN MOC mesclando do MASTER salvar durante a notificação DidSave.

Isso deve funcionar, então estou curioso para saber onde está "pendurado". Notarei que você não está executando no thread principal do MOC de forma canônica (pelo menos não para iOS 5).

Além disso, você provavelmente está interessado apenas em mesclar as alterações do MOC mestre (embora pareça que seu registro é apenas para isso). Se eu fosse usar a notificação de atualização ao salvar, faria isso ...

- (void)contextChanged:(NSNotification*)notification {
    // Only interested in merging from master into main.
    if ([notification object] != masterManagedObjectContext) return;

    [mainManagedObjectContext performBlock:^{
        [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];

        // NOTE: our MOC should not be updated, but we need to reload the data as well
    }];
}

Agora, qual pode ser o seu verdadeiro problema com relação ao travamento ... você mostra duas chamadas diferentes para salvar no master. o primeiro está bem protegido em seu próprio performBlock, mas o segundo não (embora você possa estar chamando saveMasterContext em um performBlock ...

No entanto, eu também alteraria este código ...

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    // Make sure the master runs in it's own thread...
    [masterManagedObjectContext performBlock:^{
        NSError *error = nil;
        BOOL saveSuccess = [masterManagedObjectContext save:&error];
        // Handle error...
        [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
    }];
}

No entanto, observe que o MAIN é filho de MASTER. Portanto, não deve ser necessário mesclar as alterações. Em vez disso, observe o DidSave no mestre e recupere! Os dados já estão guardados em seus pais, apenas esperando que você os peça. Esse é um dos benefícios de ter os dados no pai em primeiro lugar.

Outra alternativa a ser considerada (e estou interessado em ouvir sobre seus resultados - são muitos dados) ...

Em vez de tornar o MOC de fundo um filho do MASTER, torne-o filho do MAIN.

Pegue isto. Cada vez que o BG salva, ele é automaticamente colocado no MAIN. Agora, o MAIN tem que chamar save, e o mestre tem que chamar save, mas tudo o que está fazendo é mover ponteiros ... até que o mestre salve no disco.

A beleza desse método é que os dados vão do MOC de segundo plano diretamente para o MOC de seus aplicativos (em seguida, passam para serem salvos).

alguma penalidade para o repasse, mas todo o trabalho pesado é feito no MASTER quando atinge o disco. E se você chutar esses salvamentos no master com performBlock, então o thread principal apenas envia a solicitação e retorna imediatamente.

Por favor, deixe-me saber como foi!

Jody Hagins
fonte
Excelente resposta. Vou tentar essas ideias hoje e ver o que descobri. Obrigado!
David Weiss
Impressionante! Funcionou perfeitamente! Ainda assim, vou tentar a sua sugestão de MASTER -> MAIN -> BG e ver como essa performance funciona, parece uma ideia muito interessante. Obrigado pelas ótimas ideias!
David Weiss
4
Atualizado para alterar o performBlockAndWait para performBlock. Não sei por que isso apareceu novamente na minha fila, mas quando li desta vez, era óbvio ... não tenho certeza por que deixei passar antes. Sim, performBlockAndWait é reentrante. No entanto, em um ambiente aninhado como este, você não pode chamar a versão síncrona em um contexto filho de dentro de um contexto pai. A notificação pode ser (neste caso é) enviada do contexto pai, o que pode causar um conflito. Espero que isso esteja claro para qualquer um que vier ler isso mais tarde. Obrigado, David.
Jody Hagins de
1
@DavidWeiss Você tentou MASTER -> MAIN -> BG? Estou interessado neste padrão de design e espero saber se funciona bem para você. Obrigado.
nonamelive
2
O problema com o MASTER -> MAIN -> padrão BG é quando você busca do contexto BG, ele também busca do MAIN e isso irá bloquear a IU e fazer seu aplicativo não responsivo
Rostyslav