NSOperationQueue tüm görevleri bitirdiğinde bildirim alın


93

NSOperationQueuevar waitUntilAllOperationsAreFinished, ancak bunun için eşzamanlı olarak beklemek istemiyorum. Sıra bittiğinde kullanıcı arayüzündeki ilerleme göstergesini gizlemek istiyorum.

Bunu başarmanın en iyi yolu nedir?

E-postalarımdan bildirim gönderemiyorum NSOperationçünkü hangisinin en son olacağını bilmiyorum ve [queue operations]bildirim alındığında henüz boş (veya daha kötüsü - yeniden doldurulmuş) olmayabilir.


Yanıtlar:


167

Sıranızın özelliğini gözlemlemek için KVO kullanın operations, ardından kuyruğunuzun tamamlanıp tamamlanmadığını kontrol ederek anlayabilirsiniz [queue.operations count] == 0.

KVO'yu içinde yaptığınız dosyanın bir yerinde, KVO için aşağıdaki gibi bir bağlam bildirin ( daha fazla bilgi ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Sıranızı oluşturduğunuzda şunu yapın:

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

Daha sonra bunu şu şekilde yapın 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];
    }
}

(Bu, adınızın NSOperationQueueadlı bir mülkte olduğunu varsayar queue)

Nesneniz tamamen ayrıştırılmadan önce (veya kuyruk durumunu önemsemeyi bıraktığında) bir noktada, KVO'daki kaydı şu şekilde silmeniz gerekir:

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


Ek: iOS 4.0, NSOperationQueue.operationCountbelgelere göre KVO uyumlu bir özelliğe sahiptir . Bu cevap yine de iOS 4.0'da çalışacaktır, bu nedenle geriye dönük uyumluluk için hala yararlıdır.


26
Geleceğe uygun bir kapsülleme sağladığı için özellik erişimcisini kullanmanız gerektiğini savunuyorum (örneğin kuyruğu tembel olarak başlatmaya karar verirseniz). Bir mülke kendi ivarıyla doğrudan erişmek erken optimizasyon olarak düşünülebilir, ancak bu gerçekten tam içeriğe bağlıdır. Bir mülke doğrudan ivar üzerinden erişerek kazanılan zaman, o mülke saniyede 100-1000'den fazla kez atıfta bulunmadığınız sürece (inanılmaz derecede kaba bir tahmin olarak) genellikle ihmal edilebilir olacaktır.
Nick Forge

2
Kötü KVO kullanımı nedeniyle olumsuz oy kullanma isteği. Burada açıklanan doğru kullanım: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe

19
@NikolaiRuhe Haklısınız - operationCountaynı NSOperationQueuenesneyi gözlemlemek için KVO kullanan bir sınıfı alt sınıflarken bu kodu kullanmak potansiyel olarak hatalara yol açacaktır, bu durumda bağlam bağımsız değişkenini doğru kullanmanız gerekir. Gerçekleşmesi olası değildir, ancak kesinlikle mümkündür. (Asıl sorunu açıklamak, snark + bir bağlantı eklemekten daha yararlıdır)
Nick Forge

6
Burada ilginç bir fikir buldum . Bunu NSOperationQueue'nun alt sınıfını oluşturmak için kullandım, kuyruğa eklenen her işleme bağımlı olarak ayarlanan bir NSOperation özelliği olan 'finalOpearation' ekledim. Açıkçası, bunu yapmak için addOperation'ı geçersiz kılmak zorundaydı. Ayrıca finalOperation tamamlandığında bir temsilciye mesaj gönderen bir protokol eklendi. Şimdiye kadar çalışıyor.
pnizzle

1
Çok daha iyi! Seçenekler belirtildiğinde ve removeObserver: çağrısı bir @ try / @ catch tarafından sarıldığında çok mutlu olacağım - İdeal değil ama apple dokümanları removeObserver çağrılırken hiçbir güvenlik olmadığını belirtiyor: ... if nesne gözlemci kaydına sahip değil, uygulama çökecek.
Austin

20

Bu davranışa uyan bir şey bekliyorsanız (veya arzuluyorsanız):

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>

Bir kuyruğa birkaç "kısa" işlem ekleniyorsa, bunun yerine şu davranışı görebileceğinizi bilmelisiniz (çünkü işlemler kuyruğa eklenmenin bir parçası olarak başlatılır):

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>

Projemde, bir seri NSOperationQueue'ya (yani, maxConcurrentOperationCount = 1) çok sayıda işlem eklendikten sonra ve yalnızca hepsi tamamlandığında son işlemin ne zaman tamamlandığını bilmem gerekiyordu.

Google'da "seri bir NSoperationQueue FIFO mu?" Sorusuna yanıt olarak bir Apple geliştiricisinden gelen bu ifadeyi buldum. -

Tüm işlemler aynı önceliğe sahipse (işlem bir kuyruğa eklendikten sonra değiştirilmez) ve tüm işlemler her zaman - isReady == YES işlem kuyruğuna girdiklerinde, o zaman bir seri NSOperationQueue FIFO'dur.

Chris Kane Kakao Çerçeveleri, Apple

Benim durumumda, kuyruğa son işlemin ne zaman eklendiğini bilmek mümkündür. Bu yüzden, son işlem eklendikten sonra, kuyruğun boşaltıldığına dair bildirim göndermekten başka hiçbir şey yapmayan daha düşük öncelikli başka bir işlemi kuyruğa ekliyorum. Apple'ın beyanına göre, bu, yalnızca tüm işlemler tamamlandıktan sonra yalnızca tek bir bildirimin gönderilmesini sağlar.

İşlemler, sonuncuyu tespit etmeye izin vermeyecek bir şekilde ekleniyorsa (yani, deterministik olmayan), o zaman yukarıda belirtilen KVO yaklaşımları ile gitmeniz gerektiğini düşünüyorum, daha fazla olup olmadığını tespit etmeye çalışmak için ek koruma mantığı ile işlemler eklenebilir.

:)


Merhaba, kuyruktaki her işlem, maxConcurrentOperationCount = 1 ile bir NSOperationQueue kullanılarak sona erdiğinde bildirim almanın mümkün olup olmadığını ve nasıl mümkün olduğunu biliyor musunuz?
Sefran2

@fran: İşlemlerin tamamlandığında bir bildirim göndermesini isterdim. Bu şekilde, diğer modüller gözlemci olarak kayıt olabilir ve her biri tamamlandığında yanıt verebilir. @Selector'ınız bir bildirim nesnesi alırsa, tamamlanan işlem hakkında daha fazla ayrıntıya ihtiyacınız olması durumunda bildirimi yayınlayan nesneyi kolayca alabilirsiniz.
yazılım

17

En son çalışması için diğerlerine bağımlı olan bir NSOperation eklemeye ne dersiniz?


1
İşe yarayabilir, ancak ağır bir çözümdür ve kuyruğa yeni görevler eklemeniz gerekirse yönetmek acı verir.
Kornel

bu aslında çok şık ve en çok tercih ettiğim şey! sen benim oyumsun
Yariv Nissim

1
Şahsen bu benim en sevdiğim çözüm. Diğer tüm işlemlere bağlı olan tamamlama bloğu için kolayca basit bir NSBlockOperation oluşturabilirsiniz.
Puneet Sethi

Sıra iptal edildiğinde NSBlockOperation'ın çağrılmaması sorunuyla karşılaşabilirsiniz. Bu nedenle, iptal edildiğinde bir hata oluşturan ve hata parametresiyle bir blok çağıran kendi işleminizi yapmanız gerekir.
malhal

Bu en iyi cevap!
trapper

12

Bir alternatif, GCD kullanmaktır. Bakınız , bu referans olarak kullanılmaktadır.

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

Ben böyle yaparım.

Sırayı ayarlayın ve operasyonlar özelliğindeki değişiklikler için kaydolun:

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

... ve gözlemci (bu durumda self) şunları uygular:

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

Bu örnekte "spinner" bir UIActivityIndicatorViewşeylerin olduğunu gösteriyor. Açıkçası size uyacak şekilde değiştirebilirsiniz ...


2
Bu fordöngü potansiyel olarak pahalı görünüyor (ya tüm işlemleri bir kerede iptal ederseniz? Bu, kuyruk temizlenirken ikinci dereceden performans elde etmez mi?)
Kornel

Güzel, ancak iş parçacıkları konusunda dikkatli olun, çünkü belgelere göre: "... Bir işlem kuyruğuyla ilişkili KVO bildirimleri herhangi bir iş parçacığında oluşabilir." Muhtemelen, döndürücüyü güncellemeden önce yürütme akışını ana işlem kuyruğuna taşımanız gerekir
Igor Vasilev

4

İtibariyle iOS 13.0 , operationCount ve operasyon özellikleri kaldırılmıştır. Sıranızdaki işlemlerin sayısını kendiniz takip etmek ve hepsi tamamlandığında bir Bildirim göndermek kadar basit . Bu örnek, Operation'ın eşzamansız bir alt sınıflamasıyla da çalışır.

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

Aşağıda, kolay eşzamansız işlemler için bir Operasyon alt sınıfı bulunmaktadır

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()yöntem nerede çağrılır?
iksnae

@iksnae - Cevabımı bir Operasyon sublcass ile güncelledim . Kullandığım decrementOperationCount () içinde didSet benim bir devlet değişken. Bu yardımcı olur umarım!
Caleb Lindsey

3

Bunu yapmak için bir kategori kullanıyorum.

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 + Tamamlanma.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

Kullanım :

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!)
}];

Kaynak: https://gist.github.com/artemstepanenko/7620471


Bu neden bir tamamlandı ? Bir NSOperationQueue tamamlanmıyor - yalnızca boş oluyor. Boş durum, NSOperationQueue'nun kullanım ömrü boyunca birkaç kez girilebilir.
CouchDeveloper

SetCompletion çağrılmadan önce op1 ve op2 biterse bu çalışmaz.
malhal

Mükemmel cevap, tüm işlemin başlamasıyla sıra tamamlandığında tamamlama bloğunun çağrıldığına dair sadece 1 uyarı. Başlatma işlemleri! = İşlemler tamamlandı.
Saqib Saud

Hmm eski cevap, ama eminim öyle waitUntilFinishedolmalıYES
brandonscript

2

Sıranın özelliğini gözlemlemek için KVO kullanmaya ne dersiniz operationCount? Sonra sıra boşaldığında ve ayrıca boş kalmayı bıraktığında bunu duyarsınız. İlerleme göstergesiyle uğraşmak, aşağıdakilere benzer bir şey yapmak kadar basit olabilir:

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

Bu senin için çalıştı mı? Benim başvurumda NSOperationQueue3.1'den gelen, anahtar için KVO uyumlu olmadığından şikayet ediyor operationCount.
zoul

Aslında bu çözümü bir uygulamada denemedim, hayır. OP'nin yapıp yapmadığını söyleyemem. Ancak dokümantasyon , çalışması gerektiğini açıkça belirtiyor . Bir hata raporu yazardım. developer.apple.com/iphone/library/documentation/Cocoa/…
Sixten Otto

İPhone SDK'da NSOperationQueue üzerinde operationCount özelliği yoktur (en azından 3.1.3'ten itibaren). Max OS X dokümantasyon sayfasına ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Nick Forge

1
Zaman tüm yaraları iyileştirir ... ve bazen yanlış cevaplar. İOS 4 itibariyle operationCountözellik mevcuttur.
Sixten Otto

2

Son işlemi şu şekilde ekleyin:

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

Yani:

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

}

3
görevler eşzamanlı olarak yürütüldüğünde, bu yanlış bir yaklaşımdır.
Marcin

2
Ve kuyruk iptal edildiğinde bu son işlem başlatılmaz bile.
malhal

2

ReactiveObjC ile bunun işe yaradığını görüyorum:

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

Bilginize, Sen OBEB ile bunu başarabilirsiniz dispatch_group içinde swift 3 . Tüm görevler bittiğinde bildirim alabilirsiniz.

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

Bunu kullanmak için minimum iOS sürümü nedir?
Nitesh Borad

Swift 3, iOS 8 veya üzeri sürümlerde mevcuttur.
Abhijith

0

Yeni bir tane oluşturabilir NSThreadveya arka planda bir seçici çalıştırabilir ve orada bekleyebilirsiniz. Bittiğinde, NSOperationQueuekendi bildiriminizi gönderebilirsiniz.

Şöyle bir şey düşünüyorum:

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

...

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

Sırf uyutmak için iplik yaratmak biraz aptalca görünüyor.
Kornel

Katılıyorum. Yine de başka bir yol bulamadım.
pgb

Sadece bir iş parçacığının beklediğinden nasıl emin olursunuz? Bayrağı düşündüm, ancak bunun yarış koşullarına karşı korunması gerekiyor ve zevkime göre çok fazla NSLock kullandım.
Kornel

NSOperationQueue'yu başka bir nesneye sarabileceğinizi düşünüyorum. Bir NSOperation'ı sıraya koyduğunuzda, bir numarayı artırır ve bir iş parçacığı başlatırsınız. Bir iş parçacığı bittiğinde, bu sayıyı birer birer azaltırsınız. Önceden her şeyi sıraya koyabileceğiniz ve sonra sırayı başlatabileceğiniz bir senaryo düşünüyordum, böylece yalnızca bir bekleyen iş parçacığına ihtiyacınız olacaktı.
pgb

0

Bu İşlemi temel sınıfınız olarak whenEmpty {}kullanırsanız, OperationQueue'ya blok iletebilirsiniz :

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' türünün değerinde üye yok 'whenEmpty'
Dale

@Dale bağlantıya tıklarsanız, sizi her şeyin açıklandığı bir github sayfasına götürür. Doğru hatırlıyorsam, yanıt, Foundation'ın OperationQueue'su hala NSOperationQueue olarak adlandırıldığında yazılmıştır; belki daha az belirsizlik vardı.
user1244109

Benim hatam ... Yukarıdaki "OperationQueue" nun Swift 4'ün "OperationQueue" olduğu yanlış sonucuna vardım.
Dale

0

KVO olmadan

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

Buraya kombine ile bir çözüm arıyorsanız - sadece kendi durum nesnemi dinledim.

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

sub = self.$state.sink(receiveValue: { (state) in
 print("state updated: \(state)")
})
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.