Nhận thông báo khi NSOperationQueue hoàn thành tất cả các tác vụ


92

NSOperationQueuewaitUntilAllOperationsAreFinished, nhưng tôi không muốn đợi đồng bộ cho nó. Tôi chỉ muốn ẩn chỉ báo tiến trình trong giao diện người dùng khi hàng đợi kết thúc.

Cách tốt nhất để thực hiện điều này là gì?

Tôi không thể gửi thông báo từ NSOperations của mình vì tôi không biết thông báo nào sẽ đến cuối cùng và [queue operations]có thể chưa để trống (hoặc tệ hơn - được sắp xếp lại) khi nhận được thông báo.


Kiểm tra này nếu bạn đang sử dụng GCD trong nhanh chóng 3. stackoverflow.com/a/44562935/1522584
Abhijith

Câu trả lời:


166

Sử dụng KVO để quan sát thuộc operationstính của hàng đợi của bạn, sau đó bạn có thể biết liệu hàng đợi của mình đã hoàn thành hay chưa bằng cách kiểm tra [queue.operations count] == 0.

Ở đâu đó trong tệp bạn đang thực hiện KVO, hãy khai báo ngữ cảnh cho KVO như sau ( thông tin thêm ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Khi bạn thiết lập hàng đợi của mình, hãy làm như sau:

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

Sau đó, làm điều này trong 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];
    }
}

(Điều này giả định rằng của bạn NSOperationQueueđang ở trong một sản phẩm có tên queue)

Tại một số thời điểm trước khi đối tượng của bạn hoàn toàn deallocs (hoặc khi nó ngừng quan tâm đến trạng thái hàng đợi), bạn sẽ cần hủy đăng ký khỏi KVO như sau:

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


Phụ lục: iOS 4.0 có một thuộc NSOperationQueue.operationCounttính mà theo tài liệu là tuân thủ KVO. Tuy nhiên, câu trả lời này sẽ vẫn hoạt động trong iOS 4.0, vì vậy nó vẫn hữu ích cho khả năng tương thích ngược.


26
Tôi sẽ tranh luận rằng bạn nên sử dụng trình truy cập thuộc tính, vì nó cung cấp tính năng đóng gói được kiểm chứng trong tương lai (nếu bạn quyết định khởi tạo hàng đợi một cách lười biếng). Truy cập trực tiếp một thuộc tính bằng ivar của nó có thể được coi là tối ưu hóa quá sớm, nhưng nó thực sự phụ thuộc vào ngữ cảnh chính xác. Thời gian tiết kiệm được bằng cách truy cập trực tiếp vào một thuộc tính thông qua ivar của nó thường sẽ không đáng kể, trừ khi bạn đang tham chiếu đến thuộc tính đó hơn 100-1000 lần một giây (như một ước tính cực kỳ thô thiển).
Nick Forge,

2
Bị cám dỗ từ chối do sử dụng KVO không hợp lệ. Cách sử dụng đúng được mô tả tại đây: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe

19
@NikolaiRuhe Bạn nói đúng - sử dụng mã này khi phân lớp một lớp mà bản thân nó sử dụng KVO để quan sát operationCounttrên cùng một NSOperationQueueđối tượng sẽ có khả năng dẫn đến lỗi, trong trường hợp đó, bạn sẽ cần sử dụng đối số ngữ cảnh đúng cách. Nó không thể xảy ra, nhưng chắc chắn có thể. (Spelling ra vấn đề thực sự là hữu ích hơn vì thêm Snark + a link)
Nick Forge

6
Tìm thấy một ý tưởng thú vị ở đây . Tôi đã sử dụng điều đó để phân lớp NSOperationQueue, đã thêm thuộc tính NSOperation, 'finalOpearation', được đặt làm phụ thuộc của mỗi thao tác được thêm vào hàng đợi. Rõ ràng là phải ghi đè addOperation: để làm như vậy. Cũng đã thêm một giao thức gửi một tin nhắn đến một đại biểu khi finalOperation hoàn tất. Đã làm việc cho đến nay.
pnizzle,

1
Tốt hơn nhiều! Tôi sẽ rất vui khi các tùy chọn được chỉ định và lệnh removeObserver: được bao bọc bởi một @ try / @ catch - Nó không phải là lý tưởng nhưng các tài liệu apple chỉ định rằng không có sự an toàn khi gọi removeObserver: ... nếu đối tượng không có đăng ký quan sát viên, ứng dụng sẽ sụp đổ.
Austin

20

Nếu bạn đang mong đợi (hoặc mong muốn) điều gì đó phù hợp với hành vi này:

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>

Bạn nên lưu ý rằng nếu một số hoạt động "ngắn" đang được thêm vào hàng đợi, bạn có thể thấy hành vi này thay thế (vì các hoạt động được bắt đầu như một phần của việc được thêm vào hàng đợi):

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>

Trong dự án của mình, tôi cần biết khi nào hoạt động cuối cùng hoàn thành, sau khi một số lượng lớn hoạt động đã được thêm vào NSOperationQueue nối tiếp (tức là, maxConcurrentOperationCount = 1) và chỉ khi tất cả chúng đã hoàn thành.

Googling Tôi đã tìm thấy tuyên bố này từ một nhà phát triển của Apple để trả lời cho câu hỏi "có phải là NSoperationQueue FIFO nối tiếp không?" -

Nếu tất cả các hoạt động có cùng mức ưu tiên (không bị thay đổi sau khi hoạt động được thêm vào hàng đợi) và tất cả các hoạt động luôn - isReady == YES vào thời điểm chúng được đưa vào hàng đợi hoạt động, thì NSOperationQueue nối tiếp là FIFO.

Chris Kane Khung ca cao, Apple

Trong trường hợp của tôi, có thể biết khi nào thao tác cuối cùng được thêm vào hàng đợi. Vì vậy, sau khi thao tác cuối cùng được thêm, tôi thêm một thao tác khác vào hàng đợi, có mức độ ưu tiên thấp hơn, thao tác này không làm gì khác ngoài việc gửi thông báo rằng hàng đợi đã được làm trống. Theo tuyên bố của Apple, điều này đảm bảo rằng chỉ có một thông báo duy nhất được gửi sau khi tất cả các hoạt động đã hoàn thành.

Nếu các hoạt động đang được thêm vào theo cách không cho phép phát hiện cái cuối cùng, (tức là không xác định) thì tôi nghĩ bạn phải thực hiện với các phương pháp KVO được đề cập ở trên, với logic bảo vệ bổ sung được thêm vào để cố gắng phát hiện nếu xa hơn các hoạt động có thể được thêm vào.

:)


Xin chào, bạn có biết nếu và làm thế nào để có thể được thông báo khi mỗi hoạt động trong hàng đợi kết thúc bằng cách sử dụng NSOperationQueue với maxConcurrentOperationCount = 1?
Sefran2

@fran: Tôi sẽ yêu cầu các hoạt động đăng thông báo sau khi hoàn thành. Bằng cách đó, các mô-đun khác có thể đăng ký với tư cách là người quan sát và phản hồi khi mỗi mô-đun hoàn thành. Nếu @selector của bạn có một đối tượng thông báo, bạn có thể dễ dàng truy xuất đối tượng đã đăng thông báo, trong trường hợp bạn cần thêm chi tiết về những gì op vừa hoàn thành.
phần mềm phát triển vào

17

Làm thế nào về việc thêm một NSOperation phụ thuộc vào tất cả những người khác để nó chạy lâu dài?


1
Nó có thể hoạt động, nhưng đó là một giải pháp nặng và sẽ rất khó quản lý nếu bạn cần thêm các tác vụ mới vào hàng đợi.
Kornel

cái này thực sự rất thanh lịch và là cái tôi thích nhất! bạn bầu chọn của tôi.
Yariv Nissim

1
Cá nhân đây là giải pháp yêu thích của tôi. Bạn có thể dễ dàng tạo một NSBlockOperation đơn giản cho khối hoàn thành phụ thuộc vào tất cả các thao tác khác.
Puneet Sethi

Bạn có thể gặp sự cố mà NSBlockOperation không được gọi khi hàng đợi bị hủy. Vì vậy, bạn cần thực hiện thao tác của riêng mình tạo ra lỗi khi bị hủy và gọi một khối có tham số lỗi.
malhal

Đây là câu trả lời tốt nhất!
trapper

12

Một thay thế là sử dụng GCD. Tham khảo đây là tài liệu tham khảo.

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

Đây là cách tôi làm điều đó.

Thiết lập hàng đợi và đăng ký các thay đổi trong thuộc tính hoạt động:

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

... và người quan sát (trong trường hợp này self) thực hiện:

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

Trong ví dụ này, "spinner" là UIActivityIndicatorViewcho thấy có điều gì đó đang xảy ra. Rõ ràng là bạn có thể thay đổi để phù hợp với ...


2
Đó là forvòng lặp dường như có khả năng đắt tiền (nếu bạn hủy toàn bộ hoạt động cùng một lúc sẽ không thấy được hiệu suất bậc hai khi hàng đợi đang được dọn dẹp?)
Kornel

Tốt thôi, nhưng hãy cẩn thận với các luồng, bởi vì, theo tài liệu: "... Thông báo KVO được liên kết với hàng đợi hoạt động có thể xuất hiện trong bất kỳ luồng nào." Có lẽ, bạn sẽ cần phải di chuyển dòng chảy thực hiện vào hàng đợi hoạt động chính trước khi cập nhật spinner
Igor Vasilev

3

Tôi đang sử dụng một danh mục để làm điều này.

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

Cách sử dụng :

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

Nguồn: https://gist.github.com/artemstepanenko/7620471


Tại sao đây là một sự hoàn thành ? NSOperationQueue không hoàn thành - nó chỉ bị trống. Trạng thái trống có thể được nhập nhiều lần trong suốt thời gian tồn tại của NSOperationQueue.
CouchDeveloper

Điều này không hoạt động nếu op1 và op2 kết thúc trước khi setCompletion được gọi.
malhal

Câu trả lời tuyệt vời, chỉ cần 1 cảnh báo rằng khối hoàn thành được gọi khi hàng đợi được thực hiện với việc bắt đầu tất cả hoạt động. Bắt đầu hoạt động! = Các hoạt động đã hoàn tất.
Saqib Saud

Hmm câu trả lời cũ, nhưng tôi muốn đặt cược waitUntilFinishednênYES
brandonscript

3

Tính đến iOS 13.0 , các operationCounthoạt động bất động sản đang bị phản đối. Thật đơn giản để tự theo dõi số lượng hoạt động trong hàng đợi của bạn và kích hoạt Thông báo khi tất cả chúng đã hoàn thành. Ví dụ này cũng hoạt động với một lớp con không đồng bộ của Hoạt động .

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

Dưới đây là một lớp con của Hoạt động cho các hoạt động không đồng bộ dễ dàng

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

}


nơi được decrementOperationCount()phương pháp gọi?
iksnae

@iksnae - Tôi đã cập nhật câu trả lời của mình với một nhóm nhỏ Hoạt động . Tôi sử dụng decmentOperationCount () trong didSet của biến trạng thái của tôi . Hi vọng điêu nay co ich!
Caleb Lindsey

2

Điều gì về việc sử dụng KVO để quan sát thuộc operationCounttính của hàng đợi? Sau đó, bạn sẽ nghe về nó khi hàng đợi trống, và cả khi hàng không còn trống. Đối phó với chỉ báo tiến trình có thể đơn giản như chỉ làm một cái gì đó như:

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

Điều này đã làm việc cho bạn? Trong ứng dụng của tôi, NSOperationQueuetừ 3.1 phàn nàn rằng nó không tuân thủ KVO cho khóa operationCount.
zoul

Tôi đã không thực sự thử giải pháp này trong một ứng dụng, không. Không thể nói liệu OP đã làm. Nhưng tài liệu nói rõ rằng nó sẽ hoạt động. Tôi sẽ gửi một báo cáo lỗi. developer.apple.com/iphone/library/documentation/Cocoa/…
Sixten Otto

Không có thuộc tính operationCount trên NSOperationQueue trong SDK iPhone (ít nhất là không phải như 3.1.3). Bạn chắc hẳn đã xem trang tài liệu của Max OS X ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Nick Forge

1
Thời gian chữa lành mọi vết thương ... và đôi khi trả lời sai. Kể từ iOS 4, thuộc operationCounttính này đã có mặt.
Sixten Otto

2

Thêm thao tác cuối cùng như:

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

Vì thế:

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

}

3
khi các tác vụ được thực thi đồng thời thì đó là cách tiếp cận sai.
Marcin

2
Và khi hàng đợi bị hủy, hoạt động cuối cùng này thậm chí không được bắt đầu.
malhal

2

Với ReactiveObjC, tôi thấy điều này hoạt động tốt:

// 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, Bạn có thể đạt được điều này với GCD dispatch_group trong nhanh chóng 3 . Bạn có thể nhận được thông báo khi tất cả các nhiệm vụ hoàn thành.

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

Phiên bản iOS tối thiểu để sử dụng cái này là gì?
Nitesh Borad

Nó có sẵn từ swift 3, iOS 8 trở lên.
Abhijith

0

Bạn có thể tạo mới NSThreadhoặc thực thi một bộ chọn trong nền và đợi ở đó. Khi NSOperationQueuekết thúc, bạn có thể gửi một thông báo của riêng mình.

Tôi đang nghĩ về điều gì đó như:

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

...

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

Có vẻ hơi ngớ ngẩn khi tạo luồng chỉ để đưa nó vào trạng thái ngủ.
Kornel

Tôi đồng ý. Tuy nhiên, tôi không thể tìm ra cách khác để giải quyết nó.
pgb

Làm thế nào bạn đảm bảo rằng chỉ có một luồng đang chờ? Tôi đã nghĩ về cờ, nhưng điều đó cần được bảo vệ trước các điều kiện chủng tộc, và tôi đã sử dụng quá nhiều NSLock đối với sở thích của mình.
Kornel

Tôi nghĩ rằng bạn có thể bọc NSOperationQueue trong một số đối tượng khác. Bất cứ khi nào bạn xếp hàng một NSOperation, bạn tăng một số và khởi chạy một chuỗi. Bất cứ khi nào một chủ đề kết thúc, bạn giảm số đó đi một. Tôi đã nghĩ đến một kịch bản mà bạn có thể xếp hàng trước mọi thứ, sau đó bắt đầu hàng đợi, vì vậy bạn sẽ chỉ cần một chuỗi chờ.
pgb

0

Nếu bạn sử dụng Thao tác này làm lớp cơ sở của mình, bạn có thể chuyển whenEmpty {}khối cho OperationQueue :

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

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

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

Giá trị của loại 'OperationQueue' không có thành viên 'whenEmpty'
Dale

@Dale nếu bạn nhấp vào liên kết, nó sẽ đưa bạn đến trang github nơi mọi thứ được giải thích. Nếu tôi nhớ chính xác, câu trả lời đã được viết khi OperationQueue của Foundation vẫn được gọi là NSOperationQueue; vì vậy có thể ít mơ hồ hơn.
user1244109

Tệ thật ... Tôi đã kết luận sai rằng "OperationQueue" ở trên là "OperationQueue" của Swift 4.
Dale

0

Không có 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

Nếu bạn đến đây để tìm kiếm một giải pháp với kết hợp - tôi đã kết thúc chỉ lắng nghe đối tượng trạng thái của riêng tôi.

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

sub = self.$state.sink(receiveValue: { (state) in
 print("state updated: \(state)")
})
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.