Вопрос по grand-central-dispatch, ios, core-data – Основные данные, сохраняющие объекты в фоновом режиме

6

Что я'Я пытаюсь сделать это в двух словах: я использую фоновую очередь для сохранения объектов JSON, извлеченных из веб-службы, в базу данных Core Data Sqlite3. Сохранение происходит в сериализованной фоновой очереди I 'созданный через GCD и сохраненный во вторичный экземпляр NSManagedObjectContext, созданный для этой фоновой очереди. После завершения сохранения мне нужно обновить экземпляр NSManagedObjectContext, который находится в главном потоке, с помощью вновь созданных / обновленных объектов. Однако у меня проблема в том, что экземпляр NSManagedObjectContext в основном потоке не может найти объекты, которые были сохранены в фоновом контексте. Ниже приведен список действий, которые яберу с примерами кода. Любые мысли о том, что яя делаю неправильно?

Создайте фоновую очередь через GCD, запустите всю логику предварительной обработки и затем сохраните фоновый контекст в этом потоке :.

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    if (savedObjectIDs.count > 0) {
        [savedObjectIDs removeAllObjects];
    }
    if (savedObjectClass) {
        savedObjectClass = nil;
    }

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path, and return all created/updated objects in an array
    NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (objectIds) {
        [savedObjectIDs addObjectsFromArray:objectIds];
    }
    if (completion) {
        saveCompletionBlock = completion;
    }
    if (managedObjectClass) {
        savedObjectClass = managedObjectClass;
    }

    // save all changes object context
    [self saveManagedObjectContext];
});

"saveManagedObjectContext» Метод в основном смотрит, какой поток запущен, и сохраняет соответствующий контекст. Я проверил, что этот метод работает правильно, поэтому я не буду размещать здесь код.

Весь этот код находится в синглтоне и в синглтонеs init метод, я добавляю слушателя для "NSManagedObjectContextDidSaveNotification» и он вызывает метод mergeChangesFromContextDidSaveNotification: метод.

// merge changes from the context did save notification to the main context
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification
{
    NSThread *currentThread = [NSThread currentThread];

    if ([currentThread.name isEqual:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]) {

        // merge changes to the primary context, and wait for the action to complete on the main thread
        [_managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

        // on the main thread fetch all new data and call the completion block
        dispatch_async(dispatch_get_main_queue(), ^{

            // get objects from the database
            NSMutableArray *objects = [[NSMutableArray alloc] init];
            for (id objectID in savedObjectIDs) {
                NSError *error;
                id object = [_managedObjectContext existingObjectWithID:objectID error:&error];
                if (error) {
                    [self logError:error];
                } else if (object) {
                    [objects addObject:object];
                }
            }

            // remove all saved object IDs from the array
            [savedObjectIDs removeAllObjects];
            savedObjectClass = nil;

            // call the completion block
            //completion(objects);
            saveCompletionBlock(objects);

            // clear the saved completion block
            saveCompletionBlock = nil;
        });
    }
}

Как вы можете видеть в методе выше, я называю "mergeChangesFromContextDidSaveNotification:» в главном потоке, и я установил действие, чтобы ждать, пока не будет сделано. Согласно документации Apple, фоновый поток должен дождаться завершения этого действия, прежде чем он продолжит работу с остальной частью кода ниже этого вызова. Как я упоминал выше, когда я запускаю этот код, кажется, что все работает, но когда я пытаюсь распечатать выбранные объекты на консоли, я неничего не вернуть. Кажется, что слияние на самом деле не происходит или, возможно, не завершается до запуска остальной части моего кода. Есть ли другое уведомление, которое я должен прослушать, чтобы убедиться, что слияние завершено? Или мне нужно сохранить контекст основного объекта после слияния, но до fecth?

Кроме того, я прошу прощения за плохое форматирование кода, но кажется, что SO 'кодовые тегит как определения методов.

Спасибо, парни!

UPDATE: Я

Мы внесли изменения, которые были рекомендованы ниже, но с той же проблемой. Ниже обновленный код у меня есть.

Это код, который вызывает процессы сохранения фонового потока

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    if (savedObjectIDs.count > 0) {
        [savedObjectIDs removeAllObjects];
    }
    if (savedObjectClass) {
        savedObjectClass = nil;
    }

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path
    NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (objectIds) {
        [savedObjectIDs addObjectsFromArray:objectIds];
    }
    if (completion) {
        saveCompletionBlock = completion;
    }
    if (managedObjectClass) {
        savedObjectClass = managedObjectClass;
    }

    // listen for the merge changes from context did save notification
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // save all changes object context
    [self saveManagedObjectContext];
});

Это код, который вызывается с помощью уведомления NSManagedObjectContextDidSaveNotification

    // merge changes from the context did save notification to the main context
- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    // kill the listener
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    NSThread *currentThread = [NSThread currentThread];

    // merge changes to the primary context, and wait for the action to complete on the main thread
    [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

    // dispatch the completion block
    dispatch_async(dispatch_get_main_queue(), ^{

        // get objects from the database
        NSMutableArray *objects = [[NSMutableArray alloc] init];
        for (id objectID in savedObjectIDs) {
            NSError *error;
            id object = [[self managedObjectContext] existingObjectWithID:objectID error:&error];
            if (error) {
                [self logError:error];
            } else if (object) {
                [objects addObject:object];
            }
        }

        // remove all saved object IDs from the array
        [savedObjectIDs removeAllObjects];
        savedObjectClass = nil;

        // call the completion block
        //completion(objects);
        saveCompletionBlock(objects);

        // clear the saved completion block
        saveCompletionBlock = nil;
    });
}

ОБНОВИТЬ:

Итак, я нашел решение. Оказывается, что способ, которым я сохранял идентификаторы объектов в фоновом потоке, а затем пытался использовать их в основном потоке для их повторного извлечения, не былт работает Так что я закончил тем, что извлек вставленные / обновленные объекты из словаря userInfo, который отправляется с уведомлением NSManagedObjectContextDidSaveNotification. Ниже мой обновленный код, который сейчас работает.

Как и прежде, этот код начинает логику предварительного поиска и сохранения

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    [self logMessage:[NSString stringWithFormat:@"(%@) saveJSONObjects:objectMapping:class:completion:", [managedObjectClass description]]];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path
    [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (completion) {
        saveCompletionBlock = completion;
    }

    // listen for the merge changes from context did save notification
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // save all changes object context
    [self saveManagedObjectContext];
});

Это модифицированный метод, который обрабатывает NSManagedObjectContextDidSaveNotification

- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    // kill the listener
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // merge changes to the primary context, and wait for the action to complete on the main thread
    [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

    // dispatch the completion block
    dispatch_async(dispatch_get_main_queue(), ^{

        // pull the objects that were saved from the notification so we can get them on the main thread MOC
        NSDictionary *userInfo = [notification userInfo];
        NSMutableArray *modifiedObjects = [[NSMutableArray alloc] init];
        NSSet *insertedObject = (NSSet *)[userInfo objectForKey:@"inserted"];
        NSSet *updatedObject = (NSSet *)[userInfo objectForKey:@"updated"];

        if (insertedObject && insertedObject.count > 0) {
            [modifiedObjects addObjectsFromArray:[insertedObject allObjects]];
        }
        if (updatedObject && updatedObject.count > 0) {
            [modifiedObjects addObjectsFromArray:[updatedObject allObjects]];
        }

        NSMutableArray *objects = [[NSMutableArray alloc] init];

        // iterate through the updated objects and find them in the main thread MOC
        for (NSManagedObject *object in modifiedObjects) {
            NSError *error;
            NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
            if (error) {
                [self logError:error];
            }
            if (obj) {
                [objects addObject:obj];
            }
        }

        modifiedObjects = nil;

        // call the completion block
        saveCompletionBlock(objects);

        // clear the saved completion block
        saveCompletionBlock = nil;
    });
}

Ваш Ответ

3   ответа
7

Я тебя вижу'Мы разработали ответ, который работает для вас. Но у меня были некоторые подобные проблемы, и я хотел поделиться своим опытом и посмотреть, будет ли он вообще полезен для вас или других людей, рассматривающих эту ситуацию.

Многопоточные материалы Core Data всегда немного сбивают с толку, поэтому, пожалуйста, извините, если я неправильно прочитал ваш код. Но, похоже, для вас может быть более простой ответ.

Основная проблема, с которой вы столкнулись в первой попытке, заключается в том, что вы сохранили идентификаторы управляемых объектов (предположительно, идентификаторы объектов, которые можно передавать между потоками) в глобальную переменную для использования в основном потоке. Вы сделали это в фоновом потоке. Проблема была в том, что вы делали это ДО сохранения в фоновом потокеС контекстом управляемого объекта. Идентификаторы объектов небезопасно передавать в другую пару потока / контекста до сохранения. Они могут измениться, когда вы сохраните. Смотрите предупреждение в документации objectID:NSManagedObject reference

Вы исправили это, уведомив свой фоновый поток о сохранении, а внутри этого потока, извлекая идентификаторы объекта, теперь безопасного для использования, потому что контекст был сохранен, из объекта уведомления. Они были переданы в основной поток, и фактические изменения также были объединены в основной поток с вызовом mergeChangesFromContextDidSaveNotification. Вот'где вы можете сохранить шаг или два.

Вы регистрируетесь, чтобы услышать NSManagedObjectContextDidSaveNotification нафон нить. Вы можете зарегистрироваться, чтобы услышать то же самое уведомление наглавный нить вместо. И в этом уведомлении у вас будут те же идентификаторы объектов, которые можно безопасно использовать в главном потоке. МОК основного потока можно безопасно обновить, используя mergeChangesFromContextDidSaveNotification и переданный объект уведомления, поскольку метод предназначен для работы следующим образом:объединить изменения документов, Вызов вашего блока завершения из любого потока теперь безопасен, если вы сопоставляете moc с потоком, в котором вызывается блок завершения.

Таким образом, вы можете выполнять все обновления основного потока в главном потоке, аккуратно разделяя потоки и избегая необходимости упаковывать и перепаковывать обновленный материал или делая двойное сохранение тех же изменений в постоянном хранилище.

Чтобы быть ясным - слияние, которое происходит, находится в управляемом объекте в контексте его состояния в памяти - moc в основном потоке обновляется, чтобы соответствовать тому в фоновом потоке, но новое сохранение не выполняетсяЭто необходимо, так как вы УЖЕ сохранили эти изменения в хранилище в фоновом потоке. У вас есть потокобезопасный доступ к любому из этих обновленных объектов в объекте уведомления, так же, как вы это делали, когда использовали его в фоновом потоке.

Я надеюсь, что ваше решение работает на вас, и вы неНе нужно переоценивать - но хотел добавить свои мысли для тех, кто может увидеть это. Пожалуйста, дайте мне знать, если ямы неправильно истолковали твой код и яЯ исправлю.

4

в вашем случае, потому что при записи в фоновый moc уведомление для mergeChangesFromContextDidSaveNotification будет приходить на фоновом moc, а не на переднем плане moc.

так что вы'Вам нужно будет зарегистрироваться для уведомлений в фоновом потоке, прибывающем в фоновый объект moc.

когда вы получаете этот вызов, вы можете отправить сообщение в основной поток moc для mergeChangesFromContextDidSaveNotification.

Эндрю

обновление: здесьОбразец, который должен работать

    //register for this on the background thread
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundMOC];

- (void)mergeChanges:(NSNotification *)notification {
    NSManagedObjectContext *mainThreadMOC = [singleton managedObjectContext];

    //this tells the main thread moc to run on the main thread, and merge in the changes there
    [mainThreadMOC performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
}
ТАК неЯ не могу создать новый ответ, поэтому я обновил исходный вопрос обновленным кодом. miken.mkndev
Да, у меня есть некоторые, но я удалил их перед публикацией своего кода. Я подумал, что это просто загромождает вещи на форуме. Я действительно нашел решение своей проблемы и собираюсь опубликовать это в новом ответе на этом форуме. Спасибо за помощь! miken.mkndev
Вы пытались добавить некоторые операторы nslog, чтобы убедиться, что методы вызываются после сохранения? в частности вызывается ли ваш метод mergeChangesFromBackground? andrew lattis
Хм ... это безумная проблема. Я'Мы сделали предложенные вами обновления, но все еще не получили новые объекты, которые были сохранены в фоновом MOC из основного потока MOC. Я'я даже обновил все мои вызовы MOC основного потока, чтобы использовать фабричный метод, чтобы убедиться, чтои все, но до сих пор нетнайти объекты, которые были сохранены. Я'Мы также распечатали массив saveObjectIDs, и он правильно получает идентификаторы. Можете ли вы увидеть что-нибудь еще, что может быть неверным? Спасибо еще раз за помощь! Также я'Мы обновили мой оригинальный пост, чтобы иметь обновленный код. miken.mkndev
25

Я собираюсь выбросить это там.Прекратите следовать рекомендациям по параллелизму, перечисленным в Руководстве по программированию основных данных., Apple не обновляла его, поскольку добавляла вложенные контексты, которые НАМНОГО проще использовать. Это видео подробно рассказывает:https://developer.apple.com/videos/wwdc/2012/?id=214

Настройте основной контекст для использования основного потока (подходит для обработки пользовательского интерфейса):

NSManagedObjectContext * context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setPersistentStoreCoordinator:yourPSC];

Для любого создаваемого вами объекта, который может выполнять параллельные операции, создайте контекст частной очереди для использования

NSManagedObjectContext * backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[backgroundContext setParentContext:context];
//Use backgroundContext to insert/update...
//Then just save the context, it will automatically sync to your primary context
[backgroundContext save:nil];

QueueConcurrencyType ссылается на очередь, контекст будет делать это ».s извлекать (сохранять и извлекать запрос) операции над. Контекст NSMainQueueConcurrencyType делает все этоs работает в главной очереди, что делает его подходящим для взаимодействия с пользовательским интерфейсом. NSPrivateQueueConcurrencyType делает это на этом 'Собственная личная очередь. Поэтому, когда вы вызываете save для backgroundContext, он объединяет еголичные данные, вызывающие parentContext с помощьюperformBlock при необходимости автоматически. Ты нене хочу звонитьperformBlock в контексте частной очереди на случай, если он окажется в главном потоке, что приведет к взаимоблокировке.

Если вы хотите по-настоящему вычурно, вы можете создать основной контекст как тип параллельного доступа к частной очереди (который подходит для фонового сохранения) с основным контекстом очереди только для вашего пользовательского интерфейса, а затем дочерние контексты основного контекста очереди для фоновых операций ( нравится импорт).

Похожие вопросы