Dapatkan pemberitahuan ketika NSOperationQueue menyelesaikan semua tugas

92

NSOperationQueuemiliki waitUntilAllOperationsAreFinished, tetapi saya tidak ingin menunggu secara bersamaan. Saya hanya ingin menyembunyikan indikator kemajuan di UI saat antrian selesai.

Apa cara terbaik untuk mencapai ini?

Saya tidak bisa mengirim notifikasi dari NSOperations saya , karena saya tidak tahu mana yang akan terakhir, dan [queue operations]mungkin belum kosong (atau lebih buruk - terisi kembali) ketika notifikasi diterima.

Kornel
sumber
Periksa ini jika Anda menggunakan GCD dengan cepat 3. stackoverflow.com/a/44562935/1522584
Abhijith

Jawaban:

166

Gunakan KVO untuk mengamati operationsproperti antrian Anda, lalu Anda dapat mengetahui apakah antrian Anda telah selesai dengan memeriksa [queue.operations count] == 0.

Di suatu tempat di file tempat Anda melakukan KVO, nyatakan konteks untuk KVO seperti ini ( info selengkapnya ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Saat Anda mengatur antrian Anda, lakukan ini:

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

Kemudian lakukan ini di 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];
    }
}

(Ini mengasumsikan bahwa Anda NSOperationQueueada di properti bernama queue)

Di beberapa titik sebelum objek Anda sepenuhnya deallocs (atau ketika berhenti peduli tentang status antrian), Anda harus membatalkan pendaftaran dari KVO seperti ini:

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


Tambahan: iOS 4.0 memiliki NSOperationQueue.operationCountproperti, yang menurut dokumen sesuai dengan KVO. Namun, jawaban ini masih akan berfungsi di iOS 4.0, jadi masih berguna untuk kompatibilitas ke belakang.

Nick Forge
sumber
26
Saya berpendapat bahwa Anda harus menggunakan pengakses properti, karena ini menyediakan enkapsulasi yang terbukti di masa mendatang (jika Anda memutuskan misalnya untuk menginisialisasi antrian dengan malas). Mengakses properti secara langsung dengan ivar dapat dianggap sebagai pengoptimalan prematur, tetapi sangat bergantung pada konteks yang tepat. Waktu yang dihemat dengan mengakses properti secara langsung melalui ivar biasanya akan dapat diabaikan, kecuali Anda mereferensikan properti itu lebih dari 100-1000 kali per detik (sebagai angka perkiraan yang sangat kasar).
Nick Forge
2
Tergoda untuk memberi suara negatif karena penggunaan KVO yang buruk. Penggunaan yang tepat dijelaskan di sini: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe
19
@NikolaiRuhe Anda benar - menggunakan kode ini saat membuat subclass kelas yang menggunakan KVO sendiri untuk mengamati objek yang operationCountsama NSOperationQueueberpotensi menyebabkan bug, dalam hal ini Anda perlu menggunakan argumen konteks dengan benar. Ini tidak mungkin terjadi, tapi pasti mungkin. (Mengeja masalah sebenarnya lebih membantu daripada menambahkan snark + tautan)
Nick Forge
6
Menemukan ide yang menarik di sini . Saya menggunakannya untuk subkelas NSOperationQueue, menambahkan properti NSOperation, 'finalOpearation', yang disetel sebagai dependen dari setiap operasi yang ditambahkan ke antrean. Jelas harus mengganti addOperation: untuk melakukannya. Juga menambahkan protokol yang mengirim pesan ke delegasi saat finalOperation selesai. Sudah bekerja sejauh ini.
pnizzle
1
Jauh lebih baik! Saya akan sangat senang ketika opsi ditentukan, dan panggilan removeObserver: dibungkus oleh @ try / @ catch - Ini tidak ideal tetapi dokumen apel menetapkan bahwa tidak ada keamanan saat memanggil removeObserver: ... if objek tidak memiliki pendaftaran pengamat, aplikasi akan macet.
Austin
20

Jika Anda mengharapkan (atau menginginkan) sesuatu yang cocok dengan perilaku ini:

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>

Anda harus menyadari bahwa jika sejumlah operasi "pendek" sedang ditambahkan ke antrian Anda mungkin melihat perilaku ini sebagai gantinya (karena operasi dimulai sebagai bagian dari penambahan ke antrian):

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>

Dalam proyek saya, saya perlu mengetahui kapan operasi terakhir selesai, setelah sejumlah besar operasi telah ditambahkan ke serial NSOperationQueue (yaitu, maxConcurrentOperationCount = 1) dan hanya ketika semuanya telah selesai.

Googling Saya menemukan pernyataan ini dari pengembang Apple sebagai tanggapan atas pertanyaan "apakah serial NSoperationQueue FIFO?" -

Jika semua operasi memiliki prioritas yang sama (yang tidak berubah setelah operasi ditambahkan ke antrian) dan semua operasi selalu - isReady == YES pada saat mereka dimasukkan ke dalam antrian operasi, maka NSOperationQueue serial adalah FIFO.

Kerangka Kakao Chris Kane, Apple

Dalam kasus saya, dimungkinkan untuk mengetahui kapan operasi terakhir ditambahkan ke antrian. Jadi setelah operasi terakhir ditambahkan, saya menambahkan operasi lain ke antrian, dengan prioritas lebih rendah, yang tidak melakukan apa-apa selain mengirim pemberitahuan bahwa antrian telah dikosongkan. Berdasarkan pernyataan Apple, ini memastikan bahwa hanya satu pemberitahuan yang dikirim hanya setelah semua operasi selesai.

Jika operasi ditambahkan dengan cara yang tidak memungkinkan untuk mendeteksi yang terakhir, (yaitu, non-deterministik) maka saya pikir Anda harus menggunakan pendekatan KVO yang disebutkan di atas, dengan logika penjaga tambahan ditambahkan untuk mencoba mendeteksi jika lebih jauh operasi dapat ditambahkan.

:)

perangkat lunak berevolusi
sumber
Hai, apakah Anda tahu jika dan bagaimana mungkin untuk diberi tahu ketika setiap operasi dalam antrian diakhiri dengan menggunakan NSOperationQueue dengan maxConcurrentOperationCount = 1?
Sefran2
@fran: Saya akan meminta operasi memposting pemberitahuan setelah selesai. Dengan cara itu, modul lain dapat mendaftar sebagai pengamat, dan merespons setiap selesai. Jika @selector Anda mengambil objek notifikasi, Anda dapat dengan mudah mengambil objek yang memposting notifikasi, jika Anda memerlukan detail lebih lanjut tentang operasi yang baru saja diselesaikan.
perangkat lunak berevolusi
17

Bagaimana jika menambahkan NSOperation yang bergantung pada semua yang lain sehingga akan berjalan terakhir?

Kebanyakan Ya
sumber
1
Ini mungkin berhasil, tetapi ini adalah solusi kelas berat, dan akan sulit untuk dikelola jika Anda perlu menambahkan tugas baru ke antrian.
Kornel
ini sebenarnya sangat elegan dan yang paling saya sukai! Anda pilih saya.
Yariv Nissim
1
Secara pribadi ini adalah solusi favorit saya. Anda dapat dengan mudah membuat NSBlockOperation sederhana untuk blok penyelesaian yang bergantung pada semua operasi lainnya.
Puneet Sethi
Anda mungkin mengalami masalah di mana NSBlockOperation tidak dipanggil saat antrian dibatalkan. Jadi, Anda perlu membuat operasi Anda sendiri yang membuat kesalahan saat dibatalkan dan memanggil blok dengan parameter kesalahan.
malhal
Inilah jawaban terbaik!
penjebak
12

Salah satu alternatifnya adalah dengan menggunakan GCD. Lihat ini sebagai referensi.

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
});
nhisyam
sumber
5

Beginilah cara saya melakukannya.

Siapkan antrian, dan daftarkan untuk perubahan di properti operasi:

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

... dan pengamat (dalam hal ini self) mengimplementasikan:

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

Dalam contoh ini "pemintal" adalah UIActivityIndicatorViewpertunjukan bahwa sesuatu sedang terjadi. Jelas Anda dapat berganti untuk menyesuaikan ...

Kris Jenkins
sumber
2
Itu forlingkaran tampaknya berpotensi mahal (apa jika Anda membatalkan semua operasi sekaligus Alangkah tidak mendapatkan kinerja kuadrat ketika antrian sedang dibersihkan??)
Kornel
Bagus, tapi hati-hati dengan utas, karena, menurut dokumentasi: "... Pemberitahuan KVO yang terkait dengan antrian operasi dapat terjadi di utas mana pun." Mungkin, Anda perlu memindahkan aliran eksekusi ke antrian operasi utama sebelum memperbarui spinner
Igor Vasilev
3

Saya menggunakan kategori untuk melakukan ini.

NSOperationQueue + Penyelesaian. 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 + Penyelesaian.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

Penggunaan :

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

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

brandonscript
sumber
Mengapa ini selesai ? NSOperationQueue tidak selesai - hanya kosong. Status kosong dapat dimasukkan beberapa kali selama masa pakai NSOperationQueue.
CouchDeveloper
Ini tidak berfungsi jika op1 dan op2 selesai sebelum setCompletion dipanggil.
malhal
Jawaban yang sangat baik, hanya 1 peringatan bahwa blok penyelesaian dipanggil ketika antrian selesai dengan memulai semua operasi. Memulai operasi! = Operasi selesai.
Saqib Saud
Hmm jawaban lama, tapi saya berani bertaruh waitUntilFinishedseharusnyaYES
brandonscript
3

Mulai iOS 13.0 , properti operationCount dan operasi tidak digunakan lagi. Sangat mudah untuk melacak sendiri jumlah operasi dalam antrean Anda dan mengaktifkan Notifikasi ketika semuanya telah selesai. Contoh ini juga bekerja dengan subclass Asynchronous dari 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
    }
}

Di bawah ini adalah subkelas Operasi untuk pengoperasian asinkron yang mudah

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

}

Caleb Lindsey
sumber
dimana decrementOperationCount()metode dipanggil?
iksnae
@iksnae - Saya telah memperbarui jawaban saya dengan sublcass Operasi . Saya menggunakan decrementOperationCount () dalam didSet variabel status saya . Semoga ini membantu!
Caleb Lindsey
2

Bagaimana dengan menggunakan KVO untuk mengamati operationCountproperti antrian? Kemudian Anda akan mendengarnya saat antrean kosong, dan juga saat antrean sudah tidak lagi kosong. Berurusan dengan indikator kemajuan mungkin sesederhana melakukan sesuatu seperti:

[indicator setHidden:([queue operationCount]==0)]
Sixten Otto
sumber
Apakah ini berhasil untuk Anda? Dalam aplikasi saya, NSOperationQueuedari 3.1 mengeluh bahwa itu tidak sesuai dengan KVO untuk kuncinya operationCount.
zoul
Saya tidak benar-benar mencoba solusi ini di aplikasi, tidak. Tidak dapat mengatakan apakah OP melakukannya. Tetapi dokumentasinya dengan jelas menyatakan bahwa itu harus berfungsi. Saya akan mengajukan laporan bug. developer.apple.com/iphone/library/documentation/Cocoa/…
Sixten Otto
Tidak ada properti operationCount di NSOperationQueue di iPhone SDK (setidaknya tidak pada 3.1.3). Anda pasti telah melihat halaman dokumentasi Max OS X ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Nick Forge
1
Waktu menyembuhkan semua luka ... dan terkadang jawaban yang salah. Mulai iOS 4, operationCountproperti tersebut sudah ada.
Sixten Otto
2

Tambahkan operasi terakhir seperti:

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

Begitu:

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

}
pvllnspk.dll
sumber
3
ketika tugas dijalankan secara bersamaan maka itu adalah pendekatan yang salah.
Marcin
2
Dan ketika antrian dibatalkan, operasi terakhir ini bahkan tidak dimulai.
malhal
2

Dengan ReactiveObjC saya menemukan ini berfungsi dengan baik:

// 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!");
    }
}];
Cantik
sumber
1

FYI, Anda dapat mencapai ini dengan dispatch_group GCD dalam 3 cepat . Anda bisa mendapatkan pemberitahuan ketika semua tugas telah selesai.

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")
}
Abhijith
sumber
Berapa versi iOS minimum untuk menggunakan ini?
Nitesh Borad
Ini tersedia dari swift 3, iOS 8 atau lebih tinggi.
Abhijith
0

Anda dapat membuat yang baru NSThread, atau menjalankan selektor di latar belakang, dan menunggu di sana. Jika sudah NSOperationQueueselesai, Anda bisa mengirimkan notifikasi Anda sendiri.

Saya sedang memikirkan sesuatu seperti:

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

...

- (void)waitForQueue {
    [operationQueue waitUntilAllOperationsAreFinished];
    [[NSNotificationCenter defaultCenter] postNotification:@"queueFinished"];
}
pgb
sumber
Tampaknya agak konyol membuat utas hanya untuk menidurkannya.
Kornel
Saya setuju. Tetap saja, saya tidak bisa menemukan jalan lain.
pgb
Bagaimana Anda memastikan bahwa hanya satu utas yang menunggu? Saya berpikir tentang bendera, tetapi itu perlu dilindungi dari kondisi balapan, dan saya akhirnya menggunakan terlalu banyak NSLock untuk selera saya.
Kornel
Saya pikir Anda dapat membungkus NSOperationQueue di beberapa objek lain. Setiap kali Anda mengantrekan NSOperation, Anda menambah angka dan meluncurkan utas. Setiap kali utas berakhir, Anda mengurangi angka itu satu per satu. Saya sedang memikirkan skenario di mana Anda bisa mengantri semuanya sebelumnya, dan kemudian memulai antrian, jadi Anda hanya perlu satu utas tunggu.
pgb
0

Jika Anda menggunakan Operation ini sebagai kelas dasar Anda, Anda bisa meneruskan whenEmpty {}blok ke OperationQueue :

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

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

queue.whenEmpty = {
    print("all operations finished")
}
pengguna1244109
sumber
1
Nilai tipe 'OperationQueue' tidak memiliki anggota 'whenEmpty'
Dale
@Dale jika Anda mengklik tautannya, Anda akan dibawa ke halaman github tempat semuanya dijelaskan. Jika saya tidak salah ingat, jawabannya ditulis ketika OperationQueue Foundation masih disebut NSOperationQueue; jadi mungkin ada sedikit ambiguitas.
pengguna1244109
Saya buruk ... Saya membuat kesimpulan yang salah bahwa "OperationQueue" di atas adalah "OperationQueue" dari Swift 4.
Dale
0

Tanpa 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)
    }
}
kasyanov-ms
sumber
0

Jika Anda sampai di sini mencari solusi dengan menggabungkan - saya akhirnya hanya mendengarkan objek negara saya sendiri.

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

sub = self.$state.sink(receiveValue: { (state) in
 print("state updated: \(state)")
})
afanaian
sumber