Skip to content

Instantly share code, notes, and snippets.

@Mozilla9
Last active August 29, 2015 14:27
Show Gist options
  • Save Mozilla9/a800687bdf9b81469a3d to your computer and use it in GitHub Desktop.
Save Mozilla9/a800687bdf9b81469a3d to your computer and use it in GitHub Desktop.

Проблемы CoreData c многозадачностью:

  • CoreData не thread safe. NSManagedObject's нельзя передавать между потоками. Можно передавать только ObjectId's. Сущности, которые используются в главном потоке, должны загружаться также в главном потоке. Это связано с тем, что у каждой сущности свой контекст в котором она существует + каждый контекст имеет свой тип. Если это NSPrivateQueueConcurrencyType, то у них разные приватные очереди, в которых они работают, и в каком именно бэкграунд потоке будет сохранение мы знать не можем. Если изменяем сущность в одном контексте, то другие контексты не видят этих изменений. Для мержа изменений из одного контекста в другой можно использовать нотификации - каждый контекст кидает эвент когда сохраняет изменения.
  • http://www.slideshare.net/xzolian/core-data-with-multiple-managed-object-contexts
  • http://floriankugler.com/2013/04/29/concurrent-core-data-stack-performance-shootout/
  • https://medium.com/@tosinaf/core-data-reading-list-10d1eb0a89d4

Работа с CoreData без UI фризов (концепции):

Data Flow

  • Глобальный (синглтон) MOC-writer с типом NSPrivateQueueConcurrencyType. Привязан к PersistentStore. Его задача только сохранять в базу. Т.к. он имеет свою приватную Queue, то он сохраняет в бэкграунде.

  • Глобальный MOC-main с типом NSMainQueueConcurrencyType. В качестве parent у него MOC-writer и поэтому при сохранении он мержит все изменения в parent. Его задача загружать/удалять/апдейтить сущности в главном потоке. В нашем случае его стоит использовать только для NSFetchedResultsController.

    You use a fetched results controller to efficiently manage the results returned from a Core Data fetch request to provide data for a UITableView object.

  • Для выполнения каких-то действий с сущностями (создание/апдейт/удаление/конвертация их в json модельки), которые можно выполнять не в главном потоке, создаем локальный контекст MOC-Child с типом NSPrivateQueueConcurrencyType и c родительским контекстом MOC-writer. В бэкграунд блоке выполнять действия.

    [managedContext performBlock:^{ ... }]

    При сохранении все изменения будут мержиться в MOC-writer и он будет сохранять в бэкграунде.

Работа с CoreData через MagicalRecord:

Передача сущностей между контекстами:

Сущности нельзя шарить между контекстами и потоками, но можно шарить objectId сущности. Для чего это нужно - например, проставить во вновь созданную сущность какие-то relationship. Предполагаем, что сущность уже существует и сохранялась в persistent store (все новые и несохраненные объекты имеют временный objectId и доступны только в контексте где были созданы. Временный objectId будет изменен при сохранении) . Т.о. если сущность еще не сохранялась, то из другого контекста ее нельзя будет подгрузить.

NSManagedObjectID *objectID = addressEntity.objectID;

// Варианты подгрузки сущности по id в другом контексте
dispatch_async(my_private_queue, ^{
    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_context];
  
    // returns the object for the specified ID if it is registered in the context already or nil. It never performs I/O.
    NSManagedObject *addressEntity = [localContext objectRegisteredForID:objectID];
  
    or
  
    // returns the object for the specified ID if it is already registered, otherwise it creates a fault corresponding to that objectID.
    // It never returns nil, and never performs I/O.
    // The object specified by objectID is assumed to exist, and if that assumption is wrong the fault may throw an exception when used.
    NSManagedObject *addressEntity = [localContext objectWithID:objectID];
  
    or
  
    // returns the object for the specified ID if it is already registered in the context, or faults the object into the context.
    // It might perform I/O if the data is uncached.  If the object cannot be fetched, or does not exist, or cannot be faulted, it returns nil.
    // Unlike -objectWithID: it never returns a fault.
    NSManagedObject *addressEntity = [localContext existingObjectWithID:objectID error:&error];
});
Загрузка сущностей:
  • при показе желательно использовать paging, т.е. подгружать сущности для показа постепенно. Конвертация сущностей в модельки может занимать довольно большое время, поэтому это можно разбить на два этапа - быстро сконвертировать небольшую часть сущностей, которые будут видимы на экране, и отдать их в UI. А остальные доконвертить в бэкграунде.
  • Загрузку выполнять через локальный контекст (кроме случаев когда используется NSFetchedResultsController , то проставлять ему [NSManagedObjectContext MR_defaultContext]).
    // Создаем конекст с типом `NSPrivateQueueConcurrencyType.`
    // Parent-ом будет root saving context.
    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_context];
    
    NSPredicate *predicate = [NSPredicate predicateWithFormat:...];
    
    // Perform block in a background thread
    [localContext performBlock:^{
        // Fetch entities
        NSArray *addressesEntity = [TaxiFavouriteAddressEntity MR_findAllWithPredicate:predicate inContext:localContext];
      
        // Convert to models
        NSMutableArray *addresses = [NSMutableArray array];
        for (TaxiFavouriteAddressEntity *entity in addressesEntity) {
            addresses addObject:[entity toDictionary];
        }
      
        dispatch_async(dispatch_get_main_queue(), ^{
            // apply fetched addresses
        });
    }];
    
Импорт сущностей (создание и сохранение):
  • импорт сущностей разбиваем на батчи (размер батчей подбираем эмпирически - 10..15..100 сущностей за раз). Не надо создавать сотни сущностей и потом сохранять их скопом - операция записи на диск займет время. Нужно сохранять на диск небольшими порциями.

  • Пример импорта:

    - (void)importFavouriteAddresses:(NSArray *)addresses completion:(void (^)())completion {
      NSBlockOperation *op = [[NSBlockOperation alloc] init];
    
      op.completionBlock = ^{
          dispatch_async(dispatch_get_main_queue(), ^{
              if (completion) {
                  completion();
              }
          });
      };
    
      const NSUInteger batchSize = 15;
      const NSUInteger addressesSize = addresses.count;
      NSUInteger index = 0;
    
      while (index < addressesSize) {
          NSUInteger localSize = batchSize;
    
          if (index + localSize > addressesSize) {
              localSize = addressesSize - index;
          }
    
          NSArray *subArray = [addresses subarrayWithRange:NSMakeRange(index, localSize)];
          index += localSize;
    
          [op addExecutionBlock:^{
              [MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) {
                  NSArray *newAddresses = [self __updateRelatedAddressHierarchy:subArray inContext:localContext];
                  [self __importRelatedAddressHierarchy:newAddresses inContext:localContext];
              }];
          }];
      }
          
      if (op.executionBlocks.count) {
          dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
              [op start];
          });
      } else {
          if (completion) {
              completion();
          }
      }
    }
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment