Обратный вызов NSInvocationOperation слишком рано

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

Много прочитав о GCD (и сильно запутавшись), я решил, что самым простым способом будет вызвать мой трудоемкий метод с помощью NSInvocationOperation и добавить его во вновь созданный NSOperationQueue. Вот что у меня есть:

        [self showLoadingConfirmation]; // puts HUD on screen

        // this bit takes a while to draw a large number of dots on a MKMapView            
        NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
                                                                                selector:@selector(timeConsumingOperation:)
                                                                                  object:[self lotsOfDataFromManagedObject]];

        // this fades the HUD away and removes it from the superview
        [operation setCompletionBlock:^{ [self performSelectorOnMainThread:@selector(fadeConfirmation:) withObject:loadingView waitUntilDone:YES]; }];

        NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
        [operationQueue addOperation:operation];

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

Вместо этого он показывает HUD, начинает рисовать точки на карте и затемняет HUD, продолжая рисовать точки. Согласно моим NSLogs, есть задержка около четверти секунды перед вызовом метода для затухания HUD. Тем временем рисование точек продолжается еще несколько секунд.

Что я могу сделать, чтобы он ждал завершения рисования на карте, прежде чем исчезать HUD?

Спасибо

ОТРЕДАКТИРОВАНО ДОБАВИТЬ:

Я почти добиваюсь успеха после внесения следующих изменений:

        NSInvocationOperation *showHud = [[NSInvocationOperation alloc] initWithTarget:self
                                                                              selector:@selector(showLoadingConfirmation)
                                                                                object:nil];

        NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
                                                                                selector:@selector(timeConsumingOperation:)
                                                                                  object:[self lotsOfDataFromManagedObject]];

        NSInvocationOperation *hideHud = [[NSInvocationOperation alloc] initWithTarget:self
                                                                              selector:@selector(fadeConfirmation:)
                                                                                object:loadingView];

        NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];

        NSArray *operations = [NSArray arrayWithObjects:showHud, operation, hideHud, nil];
        [operationQueue addOperations:operations waitUntilFinished:YES];

Как ни странно, он сначала вызывает timeConsumingOperation, затем showLoadingConfirmation, затем fadeConfirmation. Это согласно моим NSLogs, которые запускаются этими методами.

Поведение, которое я вижу на экране, таково: точки рисуются, и карта соответствующим образом регулирует масштаб (часть timeConsumingOperation), затем на экране появляется HUD, а затем ничего. Все три NSLog появляются мгновенно, хотя showLoadingConfirmation не происходит до тех пор, пока timeConsumingOperation не завершится, а fadeConfirmation, похоже, вообще не происходит.

Это кажется очень странным, но также предполагает, что есть способ заставить что-то произойти по завершении timeConsumingOperation.

Я попытался добавить это:

[operationQueue setMaxConcurrentOperationCount:1];

а еще это:

[showHud setQueuePriority:NSOperationQueuePriorityVeryHigh];
[operation setQueuePriority:NSOperationQueuePriorityNormal];
[hideHud setQueuePriority:NSOperationQueuePriorityVeryLow];

но они, кажется, не имеют никакого значения.


person beev    schedule 16.11.2012    source источник
comment
Итак, завершениеBlock вызывается до завершения timeConsumingOperation? Это проблема?   -  person iDev    schedule 19.11.2012
comment
Да, это проблема.   -  person beev    schedule 19.11.2012


Ответы (2)


Помните о том, что вы на самом деле делаете, устанавливая обработчик завершения для NSInvocationOperation: когда такая операция завершается, происходит следующее (из справочника класса NSOperation):

Точный контекст выполнения для вашего блока завершения не гарантируется, но обычно это вторичный поток. Следовательно, вы не должны использовать этот блок для выполнения какой-либо работы, требующей очень специфического контекста выполнения. Вместо этого вы должны передать эту работу основному потоку вашего приложения или конкретному потоку, который способен это делать. Например, если у вас есть специальный поток для координации завершения операции, вы можете использовать блок завершения для проверки связи с этим потоком.

Итак, во-первых, блок выполняется во вторичном потоке (поэтому он войдет в очередь этого потока, и когда, наконец, придет его очередь, он просто отправит другое задание в очередь основного потока). Это означает, что он будет смешиваться в такой очереди с другими ожидающими заданиями, такими как последние обновления для контактов, которые были отправлены в основную очередь из вашего селектора timeConsumingOperation:.

И здесь возникает проблема: Если вы не установите приоритеты для разных заданий, невозможно определить порядок, в котором такие задания будут окончательно обработаны, даже если вы знаете, что они были отправлены раньше вовремя . Кроме того, в вашем случае тот факт, что ваша NSInvocationOperation завершена, не обязательно означает, что все ваши объекты были нарисованы на экране к моменту вызова блока, это означает только то, что они были отправлены в Поток обновления пользовательского интерфейса будет обработан, когда придет их очередь.

Имея это в виду, и учитывая, что вы не хотите идти по пути НОД (который я бы порекомендовал попробовать еще раз, поскольку я знаю, что это нелегко в начале, но когда вы берете его в руки, вы понимаете, что это прекрасное решение) почти для всех многопоточных вещей, которые вы хотели бы делать на iPhone), я бы создал NSOperationQueue и отправил бы туда все задания (тот, который удаляет включенный HUD, но с более низким приоритетом, чем другие). Таким образом, вы гарантируете, что удаление HUD будет обработано в основной очереди после того, как все задания по «закреплению» будут выполнены, что было вашей первой целью.

person Alejandro Benito-Santos    schedule 19.11.2012
comment
Спасибо. Я пробовал делать все это в одном потоке и управлять приоритетами. Пожалуйста, посмотрите дополнительный код, который я добавил к вопросу. Он по-прежнему ведет себя так, что для меня мало смысла, однако я чувствую себя немного ближе к решению. - person beev; 19.11.2012
comment
@beev попробуйте использовать -(void)addDependency:(NSOperation *)operation, не нужно устанавливать приоритеты таким образом. И не забудьте сделать все ваши рисунки в основной теме. - person Alejandro Benito-Santos; 19.11.2012
comment
Спасибо, вроде исправили. Теперь все происходит в правильном порядке. Один вопрос: почему вы говорите, что я должен делать все рисунки на основном потоке? Это то, что я делаю, но это приводит к тому, что пользовательский интерфейс не отвечает во время рисования. Не лучше ли сделать это в отдельном потоке, чтобы пользователь мог отказаться, если он передумает во время загрузки данных? - person beev; 20.11.2012
comment
@beev Ты совершенно прав. Я просто хотел убедиться, что вы делаете именно это в своем селекторе timeConsumingOperation:, код которого не был опубликован! Если вам нужна дополнительная информация, проверьте этот вопрос SO: ">ссылка - person Alejandro Benito-Santos; 20.11.2012
comment
@beev Можете ли вы отметить это как правильный ответ, если он вам помог? Спасибо - person Alejandro Benito-Santos; 20.11.2012
comment
Спасибо за вашу помощь. Я счастлив, что сейчас все идет в правильном порядке. Разочаровывает, что я не могу заставить его работать с отдельным потоком, но, думаю, для этого мне понадобится GCD. Я пытаюсь понять GCD, но все еще чувствую себя сбитым с толку синтаксисом. - person beev; 20.11.2012

Если HUD исчезает, значит, был вызван ваш блок завершения, что означает возврат вашего метода timeConsumingOperation:.

Если вы все еще видите рисующиеся точки, это означает, что транзакции анимации или рисования все еще выполняются или все еще находятся в очереди, даже после возврата timeConsumingOperation:. Решение зависит от того, какую технику вы используете для рисования. Вы используете основную анимацию? MapKit с аннотациями?

person Community    schedule 19.11.2012
comment
Я использую mapKit и рисую несколько сотен точек, добавляя наложения MKCirle. Я подозреваю, что есть более эффективный способ рисования этих точек, но я не смог его найти. - person beev; 19.11.2012
comment
Что ж, в этом случае похоже, что MapKit добавляет/отрисовывает контакты асинхронно, что означает, что они все еще появляются даже после того, как ваш метод вернулся. К сожалению, будет очень сложно определить, когда именно они появились на экране. - person Mike Weller; 19.11.2012
comment
Спасибо. Я внес некоторые изменения в код и изменил вопрос, чтобы показать их. Результаты, кажется, предполагают, что можно заставить метод запускать метод в конце рисования всех этих точек. Просто странно, что он не вызывает метод, который я ожидаю! - person beev; 19.11.2012