Отримуйте сповіщення, коли NSOperationQueue закінчить усі завдання


92

NSOperationQueueмає waitUntilAllOperationsAreFinished, але я не хочу чекати його синхронно. Я просто хочу приховати індикатор прогресу в інтерфейсі, коли черга закінчується.

Який найкращий спосіб цього досягти?

Я не можу надсилати сповіщення з мого NSOperations, оскільки я не знаю, яке з них буде останнім, і, [queue operations]можливо, ще не буде порожнім (або ще гірше - повторно заповненим), коли отримано сповіщення.


Відзначте, якщо ви використовуєте НСД в стриже 3. stackoverflow.com/a/44562935/1522584
Абайджіт

Відповіді:


166

Використовуйте KVO, щоб спостерігати за operationsвластивістю вашої черги, тоді ви зможете визначити, чи вичерпалася ваша черга, перевіривши наявність [queue.operations count] == 0.

Десь у файлі, в якому ви робите KVO, оголосіть такий контекст для KVO ( докладніше ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Коли ви налаштовуєте свою чергу, зробіть наступне:

[self.queue addObserver:self forKeyPath:@"operations" options:0 context:&kQueueOperationsChanged];

Тоді зробіть це у своєму observeValueForKeyPath:

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
                         change:(NSDictionary *)change context:(void *)context
{
    if (object == self.queue && [keyPath isEqualToString:@"operations"] && context == &kQueueOperationsChanged) {
        if ([self.queue.operations count] == 0) {
            // Do something here when your queue has completed
            NSLog(@"queue has completed");
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object 
                               change:change context:context];
    }
}

(Це припускаючи, що ви NSOperationQueueперебуваєте у властивості з іменем queue)

У якийсь момент до того, як ваш об’єкт повністю розблокується (або коли він перестане дбати про стан черги), вам потрібно буде скасувати реєстрацію в KVO таким чином:

[self.queue removeObserver:self forKeyPath:@"operations" context:&kQueueOperationsChanged];


Додаток: iOS 4.0 має NSOperationQueue.operationCountвластивість, яка згідно з документами відповідає KVO. Ця відповідь все одно буде працювати в iOS 4.0, однак вона все ще корисна для зворотної сумісності.


26
Я стверджую, що вам слід використовувати доступ до властивостей, оскільки він забезпечує надійну інкапсуляцію (якщо ви вирішите, наприклад, ліниво ініціалізувати чергу). Безпосередній доступ до властивості за допомогою його ivar можна вважати передчасною оптимізацією, але це насправді залежить від точного контексту. Заощаджений час, безпосередньо звертаючись до власності через її ivar, зазвичай буде незначним, якщо ви не будете посилатися на цю властивість більше 100-1000 разів на секунду (як неймовірно груба оцінка).
Нік Фордж

2
Спокуса проголосувати проти через погане використання KVO. Правильне використання описано тут: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe

19
@NikolaiRuhe Ви маєте рацію - використання цього коду при підкласуванні класу, який сам використовує KVO для спостереження operationCountза тим самим NSOperationQueueоб'єктом, потенційно може призвести до помилок, і в цьому випадку вам потрібно буде правильно використовувати аргумент контексту. Це малоймовірно, але точно можливо. (Пояснення фактичної проблеми корисніше, ніж додавання snark + посилання)
Нік Фордж

6
Знайшов цікаву ідею тут . Я використав це для підкласу NSOperationQueue, додав властивість NSOperation, 'finalOpearation', яке встановлюється як залежне від кожної операції, доданої до черги. Очевидно, довелося перевизначити addOperation: для цього. Також доданий протокол, який надсилає повідомлення делегатові після завершення finalOperation. Працював до цього часу.
pnizzle

1
Набагато краще! Я буду дуже радий, коли будуть вказані параметри, а виклик removeObserver: завертається @ try / @ catch - Це не ідеально, але в документах apple вказано, що немає безпеки при виклику removeObserver: ... if об'єкт не має реєстрації спостерігача, програма потерпить крах.
Остін,

20

Якщо ви очікуєте (або бажаєте) чогось, що відповідає цій поведінці:

t=0 add an operation to the queue.  queueucount increments to 1
t=1 add an operation to the queue.  queueucount increments to 2
t=2 add an operation to the queue.  queueucount increments to 3
t=3 operation completes, queuecount decrements to 2
t=4 operation completes, queuecount decrements to 1
t=5 operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>

Вам слід пам’ятати, що якщо до черги додається ряд «коротких» операцій, ви можете побачити таку поведінку (оскільки операції запускаються як частина додавання до черги):

t=0  add an operation to the queue.  queuecount == 1
t=1  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>
t=2  add an operation to the queue.  queuecount == 1
t=3  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>
t=4  add an operation to the queue.  queuecount == 1
t=5  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>

У своєму проекті мені потрібно було знати, коли завершилась остання операція, після того, як велика кількість операцій була додана до послідовного NSOperationQueue (тобто maxConcurrentOperationCount = 1), і лише тоді, коли всі вони були завершені.

Погугливши, я знайшов це твердження розробника Apple у відповідь на запитання "чи є серійний NSoperationQueue FIFO?" -

Якщо всі операції мають однаковий пріоритет (який не змінюється після додавання операції до черги), і всі операції завжди - isReady == ТАК до моменту, коли вони потрапляють до черги операцій, тоді послідовний NSOperationQueue - це FIFO.

Кріс Кейн Какао Frameworks, Apple

У моєму випадку можна дізнатись, коли остання операція була додана до черги. Тож після додавання останньої операції я додаю до черги ще одну операцію, нижчого пріоритету, яка не робить нічого, крім як надсилає повідомлення про те, що черга була звільнена. Враховуючи заяву Apple, це гарантує, що лише одне повідомлення надсилається лише після завершення всіх операцій.

Якщо операції додаються таким чином, що не дозволяють виявити останню (тобто недетерміновану), тоді я думаю, що вам слід скористатися згаданими вище підходами KVO, додавши додаткову логіку захисту, щоб спробувати виявити, чи буде подальше можуть бути додані операції.

:)


Привіт, ти знаєш, чи можна і як отримати сповіщення про закінчення кожної операції в черзі, використовуючи NSOperationQueue з maxConcurrentOperationCount = 1?
Sefran2,

@fran: Я мав би операцію опублікувати повідомлення після завершення. Таким чином, інші модулі можуть реєструватися як спостерігачі та відповідати, коли кожен завершить. Якщо ваш @selector бере об'єкт сповіщення, ви можете легко отримати об'єкт, який розмістив повідомлення, на випадок, якщо вам потрібні додаткові відомості про те, що операція щойно виконана.
програмне забезпечення розвинулось

17

Як щодо додавання операції NSO, яка залежить від усіх інших, щоб вона працювала останньою?


1
Це може спрацювати, але це надважке рішення, і було б важко впоратися, якщо вам потрібно додати нові завдання до черги.
Корнель

це насправді дуже елегантно і саме те, що я віддав перевагу найбільше! ти мій голос.
Ярів Ніссім

1
Особисто це моє улюблене рішення. Ви можете легко створити просту NSBlockOperation для блоку завершення, яка залежить від усіх інших операцій.
Puneet Sethi

Можливо, виникла проблема, коли NSBlockOperation не викликається при скасуванні черги. Тому вам потрібно зробити власну операцію, яка створює помилку при скасуванні та викликає блок із параметром помилки.
malhal

Це найкраща відповідь!
трапер

12

Однією з альтернатив є використання GCD. Зверніться до цього як до посилання.

dispatch_queue_t queue = dispatch_get_global_queue(0,0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,queue,^{
 NSLog(@"Block 1");
 //run first NSOperation here
});

dispatch_group_async(group,queue,^{
 NSLog(@"Block 2");
 //run second NSOperation here
});

//or from for loop
for (NSOperation *operation in operations)
{
   dispatch_group_async(group,queue,^{
      [operation start];
   });
}

dispatch_group_notify(group,queue,^{
 NSLog(@"Final block");
 //hide progress indicator here
});

5

Ось як я це роблю.

Налаштуйте чергу та зареєструйтесь для змін у властивості операцій:

myQueue = [[NSOperationQueue alloc] init];
[myQueue addObserver: self forKeyPath: @"operations" options: NSKeyValueObservingOptionNew context: NULL];

... і спостерігач (у даному випадку self) реалізує:

- (void) observeValueForKeyPath:(NSString *) keyPath ofObject:(id) object change:(NSDictionary *) change context:(void *) context {

    if (
        object == myQueue
        &&
        [@"operations" isEqual: keyPath]
    ) {

        NSArray *operations = [change objectForKey:NSKeyValueChangeNewKey];

        if ( [self hasActiveOperations: operations] ) {
            [spinner startAnimating];
        } else {
            [spinner stopAnimating];
        }
    }
}

- (BOOL) hasActiveOperations:(NSArray *) operations {
    for ( id operation in operations ) {
        if ( [operation isExecuting] && ! [operation isCancelled] ) {
            return YES;
        }
    }

    return NO;
}

У цьому прикладі "спінер" UIActivityIndicatorViewпоказує, що щось відбувається. Очевидно, що ви можете змінити, щоб влаштувати ...


2
Цей forцикл здається потенційно дорогим (що, якщо скасувати всі операції одночасно? Хіба це не отримає квадратичної продуктивності при очищенні черги?)
Kornel

Хороший, але будьте обережні з потоками, оскільки, згідно з документацією: "... сповіщення KVO, пов'язані з чергою операцій, можуть виникати в будь-якому потоці." Можливо, вам потрібно було б перемістити потік виконання до основної черги операцій перед оновленням спінера
Ігор Васильєв

3

Для цього я використовую категорію.

NSOperationQueue + Завершення. H

//
//  NSOperationQueue+Completion.h
//  QueueTest
//
//  Created by Artem Stepanenko on 23.11.13.
//  Copyright (c) 2013 Artem Stepanenko. All rights reserved.
//

typedef void (^NSOperationQueueCompletion) (void);

@interface NSOperationQueue (Completion)

/**
 * Remarks:
 *
 * 1. Invokes completion handler just a single time when previously added operations are finished.
 * 2. Completion handler is called in a main thread.
 */

- (void)setCompletion:(NSOperationQueueCompletion)completion;

@end

NSOperationQueue + Завершення м

//
//  NSOperationQueue+Completion.m
//  QueueTest
//
//  Created by Artem Stepanenko on 23.11.13.
//  Copyright (c) 2013 Artem Stepanenko. All rights reserved.
//

#import "NSOperationQueue+Completion.h"

@implementation NSOperationQueue (Completion)

- (void)setCompletion:(NSOperationQueueCompletion)completion
{
    NSOperationQueueCompletion copiedCompletion = [completion copy];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self waitUntilAllOperationsAreFinished];

        dispatch_async(dispatch_get_main_queue(), ^{
            copiedCompletion();
        });
    });
}

@end

Використання :

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    // ...
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    // ...
}];

[operation2 addDependency:operation1];

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation1, operation2] waitUntilFinished:YES];

[queue setCompletion:^{
    // handle operation queue's completion here (launched in main thread!)
}];

Джерело: https://gist.github.com/artemstepanenko/7620471


Чому це завершення ? Черга NSOperationQue не завершується - вона просто порожня. Порожній стан можна вводити кілька разів протягом життя NSOperationQueue.
CouchDeveloper

Це не працює, якщо op1 та op2 закінчуються перед викликом setCompletion.
malhal

Відмінна відповідь, лише 1 застереження, що блок завершення викликається, коли черга закінчується із запуском усієї операції. Початок операцій! = Операції завершені.
Saqib Saud

Хм-м-да, стара відповідь, але я б поспорився, що так і waitUntilFinishedмає бутиYES
brandonscript

3

Станом прошивки 13,0 , то operationCount і експлуатаційні властивості є застарілими. Так само просто самостійно відстежувати кількість операцій у вашій черзі та запускати сповіщення, коли всі вони завершені. Цей приклад також працює з асинхронним підкласом операції .

class MyOperationQueue: OperationQueue {
            
    public var numberOfOperations: Int = 0 {
        didSet {
            if numberOfOperations == 0 {
                print("All operations completed.")
                
                NotificationCenter.default.post(name: .init("OperationsCompleted"), object: nil)
            }
        }
    }
    
    public var isEmpty: Bool {
        return numberOfOperations == 0
    }
    
    override func addOperation(_ op: Operation) {
        super.addOperation(op)
        
        numberOfOperations += 1
    }
    
    override func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool) {
        super.addOperations(ops, waitUntilFinished: wait)
        
        numberOfOperations += ops.count
    }
    
    public func decrementOperationCount() {
        numberOfOperations -= 1
    }
}

Нижче наведено підклас Операція для легких асинхронних операцій

class AsyncOperation: Operation {
    
    let queue: MyOperationQueue

enum State: String {
    case Ready, Executing, Finished
    
    fileprivate var keyPath: String {
        return "is" + rawValue
    }
}

var state = State.Ready {
    willSet {
        willChangeValue(forKey: newValue.keyPath)
        willChangeValue(forKey: state.keyPath)
    }
    
    didSet {
        didChangeValue(forKey: oldValue.keyPath)
        didChangeValue(forKey: state.keyPath)
        
        if state == .Finished {
            queue.decrementOperationCount()
        }
    }
}

override var isReady: Bool {
    return super.isReady && state == .Ready
}

override var isExecuting: Bool {
    return state == .Executing
}

override var isFinished: Bool {
    return state == .Finished
}

override var isAsynchronous: Bool {
    return true
}

public init(queue: MyOperationQueue) {
    self.queue = queue
    super.init()
}

override func start() {
    if isCancelled {
        state = .Finished
        return
    }
    
    main()
    state = .Executing
}

override func cancel() {
    state = .Finished
}

override func main() {
    fatalError("Subclasses must override main without calling super.")
}

}


де викликається decrementOperationCount()метод?
iksnae

@iksnae - Я оновив свою відповідь підкалкою " Операція" . Я використовую decrementOperationCount () у межах didSet моєї змінної стану . Сподіваюся, це допомагає!
Калеб Ліндсі,

2

А як щодо використання KVO для спостереження operationCountвластивості черги? Тоді ви почуєте про це, коли черга порожняє, а також коли вона перестає бути порожньою. Робота з індикатором прогресу може бути настільки простою, як просто зробити щось на зразок:

[indicator setHidden:([queue operationCount]==0)]

Це спрацювало для вас? У моїй заявці NSOperationQueueз версії 3.1 скаржиться, що вона не відповідає KVO для ключа operationCount.
zoul

Я насправді не пробував це рішення в додатку, ні. Не можу сказати, чи зробив ОП. Але в документації чітко зазначено, що це має спрацювати. Я подав би повідомлення про помилку. developer.apple.com/iphone/library/documentation/Cocoa/…
Сікстен Отто

Немає властивості operationCount у NSOperationQueue в iPhone SDK (принаймні, не станом на 3.1.3). Ви, мабуть, переглядали сторінку документації Max OS X ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Нік Фордж,

1
Час загоює всі рани ... а часом і неправильні відповіді. Станом на iOS 4 operationCountвластивість присутня.
Sixten Otto

2

Додайте останню операцію, наприклад:

NSInvocationOperation *callbackOperation = [[NSInvocationOperation alloc] initWithTarget:object selector:selector object:nil];

Так:

- (void)method:(id)object withSelector:(SEL)selector{
     NSInvocationOperation *callbackOperation = [[NSInvocationOperation alloc] initWithTarget:object selector:selector object:nil];
     [callbackOperation addDependency: ...];
     [operationQueue addOperation:callbackOperation]; 

}

3
коли завдання виконуються одночасно, це неправильний підхід.
Marcin

2
І коли чергу скасовано, ця остання операція навіть не запускається.
malhal

2

З ReactiveObjC я вважаю, що це добре працює:

// skip 1 time here to ignore the very first call which occurs upon initialization of the RAC block
[[RACObserve(self.operationQueue, operationCount) skip:1] subscribeNext:^(NSNumber *operationCount) {
    if ([operationCount integerValue] == 0) {
         // operations are done processing
         NSLog(@"Finished!");
    }
}];

1

FYI, ви можете досягти цього за допомогою GCD dispatch_group в швидкому 3 . Ви можете отримати сповіщення, коли всі завдання закінчені.

let group = DispatchGroup()

    group.enter()
    run(after: 6) {
      print(" 6 seconds")
      group.leave()
    }

    group.enter()
    run(after: 4) {
      print(" 4 seconds")
      group.leave()
    }

    group.enter()
    run(after: 2) {
      print(" 2 seconds")
      group.leave()
    }

    group.enter()
    run(after: 1) {
      print(" 1 second")
      group.leave()
    }


    group.notify(queue: DispatchQueue.global(qos: .background)) {
      print("All async calls completed")
}

Яка мінімальна версія iOS для використання цього?
Нітеш Борад

Він доступний на швидкій версії 3, iOS 8 або новішої версії.
Абхіджіт

0

Ви можете створити новий NSThreadабо виконати селектор у фоновому режимі і зачекати там. Коли NSOperationQueueзакінчить, ви можете надіслати власне повідомлення.

Я думаю про щось на зразок:

- (void)someMethod {
    // Queue everything in your operationQueue (instance variable)
    [self performSelectorInBackground:@selector(waitForQueue)];
    // Continue as usual
}

...

- (void)waitForQueue {
    [operationQueue waitUntilAllOperationsAreFinished];
    [[NSNotificationCenter defaultCenter] postNotification:@"queueFinished"];
}

Здається, трохи безглуздо створювати нитку, лише щоб приспати її.
Kornel

Я згоден. Тим не менше, я не міг знайти іншого способу обійти це.
pgb

Як би ви гарантували, що чекає лише одна нитка? Я думав про прапор, але його потрібно захищати від умов перегонів, і в підсумку я використав занадто багато NSLock на мій смак.
Kornel

Я думаю, ви можете обернути NSOperationQueue в якийсь інший об'єкт. Щоразу, коли ви ставите в чергу операцію NSO, ви збільшуєте число та запускаєте ланцюжок. Щоразу, коли потік закінчується, ви зменшуєте це число на одиницю. Я думав про сценарій, коли ви можете заздалегідь поставити все в чергу, а потім запустити чергу, тож вам знадобиться лише один потік очікування.
pgb

0

Якщо ви використовуєте цю операцію як базовий клас, ви можете передати whenEmpty {}блок OperationQueue :

let queue = OOperationQueue()
queue.addOperation(op)
queue.addOperation(delayOp)

queue.addExecution { finished in
    delay(0.5) { finished() }
}

queue.whenEmpty = {
    print("all operations finished")
}

1
Значення типу 'OperationQueue' не має члена 'whenEmpty'
Дейл,

@Dale, якщо ви натиснете на посилання, ви потрапите на сторінку github, де все пояснено. Якщо я правильно згадую, відповідь була написана, коли операційна черга Фонду все ще називалася NSOperationQueue; тож двозначності, можливо, було менше.
user1244109

Мій поганий ... Я зробив хибний висновок, що "OperationQueue" вище - це "OperationQueue" Swift 4.
Дейл

0

Без KVO

private let queue = OperationQueue()

private func addOperations(_ operations: [Operation], completionHandler: @escaping () -> ()) {
    DispatchQueue.global().async { [unowned self] in
        self.queue.addOperations(operations, waitUntilFinished: true)
        DispatchQueue.main.async(execute: completionHandler)
    }
}

0

Якщо ви приїхали сюди шукати рішення за допомогою комбінації - я в кінцевому підсумку просто слухав свій власний об'єкт стану.

@Published var state: OperationState = .ready
var sub: Any?

sub = self.$state.sink(receiveValue: { (state) in
 print("state updated: \(state)")
})
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.