NSOperationQueueがすべてのタスクを完了したときに通知を受け取ります


92

NSOperationQueue持っていますがwaitUntilAllOperationsAreFinished、私は同期的にそれを待ちたくありません。キューが終了したときにUIの進行状況インジケーターを非表示にしたいだけです。

これを達成するための最良の方法は何ですか?

NSOperationどのsが最後になるのかわからないため[queue operations]、通知を受信して​​もまだ空ではない(またはさらに悪いことに、再入力されている)ため、から通知を送信できません。


あなたはSWIFT 3でGCDを使用している場合は、これをチェックし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を介してプロパティに直接アクセスすることで節約される時間は、そのプロパティを1秒間に100〜1000回以上参照している場合(信じられないほど粗雑な推測として)でない限り、通常は無視できます。
Nick Forge

2
KVOの使用法が悪いため、反対票を投じようとしています。ここで説明されている適切な使用法:dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe

19
@NikolaiRuhe正解です。KVOを使用operationCountして同じNSOperationQueueオブジェクトを監視するクラスをサブクラス化するときにこのコードを使用すると、バグが発生する可能性があり、その場合はコンテキスト引数を適切に使用する必要があります。発生する可能性は低いですが、間違いなく可能です。(実際の問​​題を詳しく説明することは、snark +リンクを追加するよりも役立ちます)
Nick Forge

6
ここで興味深いアイデアを見つけました。これを使用してNSOperationQueueをサブクラス化し、キューに追加された各操作の依存として設定されるNSOperationプロパティ 'finalOpearation'を追加しました。そのためには、明らかにaddOperation:をオーバーライドする必要がありました。finalOperationの完了時にメッセージをデリゲートに送信するプロトコルも追加されました。これまでに取り組んできました。
pnizzle 2013年

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)、最後の操作がいつ完了したかを知る必要がありました。

グーグル「シリアルNSoperationQueue FIFOはありますか?」-

すべての操作の優先度が同じ(操作がキューに追加された後も変更されない)で、すべての操作が常に-isReady == YESの場合、操作キューに入れられるまで、シリアルNSOperationQueueはFIFOです。

クリス・ケインココアフレームワーク、アップル

私の場合、最後の操作がいつキューに追加されたかを知ることができます。したがって、最後の操作が追加された後、優先度の低い別の操作をキューに追加します。これは、キューが空になったという通知を送信するだけです。Appleの発言を踏まえると、これにより、すべての操作が完了した後にのみ、通知が1つだけ送信されます。

最後の操作を検出できない方法で操作が追加されている場合(つまり、非決定的)、上記のKVOアプローチを使用する必要があると思います。操作が追加される場合があります。

:)


こんにちは、maxConcurrentOperationCount = 1のNSOperationQueueを使用して、キュー内の各操作が終了したときに通知を受け取ることができるかどうか、またどのようにして通知が可能かを知っていますか?
Sefran2

@fran:完了時にオペレーションに通知を投稿します。そうすることで、他のモジュールをオブザーバーとして登録し、それぞれが完了すると応答することができます。@selectorが通知オブジェクトを受け取る場合、完了した操作に関する詳細が必要な場合に備えて、通知を投稿したオブジェクトを簡単に取得できます。
ソフトウェアは

17

他のすべてに依存するNSOperationを追加して、最後に実行されるようにしませんか?


1
うまくいくかもしれませんが、重いソリューションであり、キューに新しいタスクを追加する必要がある場合に管理するのは面倒です。
Kornel

これは実際には非常にエレガントで、私が最も好んだものです!あなたは私の投票。
Yariv Nissim 2013

1
個人的にこれは私のお気に入りのソリューションです。他のすべての操作に依存する完了ブロックの単純なNSBlockOperationを簡単に作成できます。
Puneet Sethi 2016

キューがキャンセルされたときにNSBlockOperationが呼び出されないという問題が発生する場合があります。したがって、キャンセルするとエラーが発生し、エラーパラメータを使用してブロックを呼び出す独自の操作を行う必要があります。
Malhal

これが一番の答えです!
トラッパー2017

12

1つの代替策は、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

これが私のやり方です。

キューを設定し、operationsプロパティの変更を登録します。

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ループは潜在的にコストがかかるようです(すべての操作を一度にキャンセルするとどうなりますか?キューがクリーンアップされているときに2次的なパフォーマンスが得られませんか?)
Kornel

ドキュメントによると、「...キューに関連付けられているKVO通知はどのスレッドでも発生する可能性があります」ので、スレッドには注意してください。おそらく、スピナーを更新する前に、実行フローをメイン操作キューに移動する必要があります
Igor Vasilev

3

これを行うためにカテゴリを使用しています。

NSOperationQueue + Completion.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 + Completion.m

//
//  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


なぜこれが完成ですか?NSOperationQueueは完了せず、単に空になります。空の状態は、NSOperationQueueの存続期間中に何度も入力できます。
CouchDeveloper 2015年

これは、setCompletionが呼び出される前にop1とop2が終了する場合は機能しません。
マルハル

すばらしい答えです。キューがすべての操作を開始して完了したときに完了ブロックが呼び出されるという警告が1つだけあります。操作の開始!=操作が完了しました。
Saqib Saud 2017

うーん古い答えですが、私はwaitUntilFinishedYES
間違い

3

とおりiOSの13.0operationCount操作性が推奨されません。キュー内の操作の数を自分で追跡し、操作がすべて完了したら通知を発行するのも同じくらい簡単です。この例は、Operationの非同期サブクラス化でも機能します。

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
    }
}

以下は、簡単な非同期操作のためのOperationのサブクラスです

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私の状態変数。お役に立てれば!
Caleb Lindsey

2

KVOを使用しoperationCountてキューのプロパティを監視するのはどうですか?次に、キューが空になったとき、およびキューが空でなくなったときにそれについて聞きます。進行状況インジケーターの処理は、次のようなことをするのと同じくらい簡単かもしれません。

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

これでうまくいきましたか?私のアプリケーションではNSOperationQueue、3.1から、キーがKVOに準拠していないと不平を言っていますoperationCount
zoul

実際にこのソリューションをアプリで試したことはありません。OPが行ったかどうかはわかりません。しかし、ドキュメントにはそれ機能するはずであること明確に述べられています。バグレポートを提出します。developer.apple.com/iphone/library/documentation/Cocoa/...
Sixtenオットー

iPhone SDKのNSOperationQueueにoperationCountプロパティはありません(少なくとも3.1.3以降)。Max OS Xのドキュメントページ(developer.apple.com/Mac/library/documentation/Cocoa/Reference/…)を見ている必要があります
Nick Forge

1
時間はすべての傷を癒します...そして時には間違った答えを癒します。iOS 4以降、このoperationCountプロパティは存在します。
Sixten Otto 2010

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、2014年

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

参考までに、Swift 3の GCD dispatch_groupでこれを実現できます。すべてのタスクが完了したときに通知を受け取ることができます。

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バージョンは何ですか?
Nitesh Borad 2017年

Swift 3、iOS 8以降で利用できます。
Abhijith

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 2009年

1つのスレッドのみが待機していることをどのように確認しますか?私は旗について考えましたが、それはレースコンディションから保護する必要があり、私の好みのためにNSLockを使いすぎました。
Kornel

NSOperationQueueを他のオブジェクトでラップできると思います。NSOperationをキューに入れるたびに、数値を増分してスレッドを起動します。スレッドが終了するたびに、その数を1つ減らします。すべてを事前にキューに入れてからキューを開始できるシナリオを考えていたので、待機中のスレッドは1つだけで済みます。
pgb 2009年

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")
}

タイプ「OperationQueue」の値にはメンバー「whenEmpty」がありません
Dale

@Daleリンクをクリックすると、すべてが説明されているgithubページに移動します。正しく思い出せば、FoundationのOperationQueueがまだNSOperationQueueと呼ばれていたときに答えが書かれました。ですから多分あいまいさが減ったのです
user1244109

私の悪いところ...上記の「OperationQueue」はSwift 4の「OperationQueue」であるという誤った結論を出しました。
デール、

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

あなたがここでcombineを使った解決策を探しているなら-私は自分の状態オブジェクトを聞くだけになりました。

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

sub = self.$state.sink(receiveValue: { (state) in
 print("state updated: \(state)")
})
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.