NSOperationQueue完成所有任务时获取通知


92

NSOperationQueuewaitUntilAllOperationsAreFinished,但我不想同步等待它。我只想在队列完成时在UI中隐藏进度指示器。

做到这一点的最佳方法是什么?

我无法从NSOperations 发送通知,因为我不知道哪个将是最后一个,并且[queue operations]在收到通知时可能还不为空(或者更糟-已重新填充)。


如果您正在快速使用GCD,请检查此项。3. stackoverflow.com/a/44562935/1522584
Abhijith,2015年

Answers:


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和链接更为有用)
Nick Forge

6
在这里找到了一个有趣的主意。我使用它作为NSOperationQueue的子类,添加了NSOperation属性“ finalOpearation”,该属性设置为对添加到队列的每个操作的依赖。显然必须重写addOperation:。还添加了一个协议,该协议在finalOperation完成时将消息发送给委托。到目前为止一直在工作。
pnizzle

1
好多了!指定选项时,我会很高兴,并且removeObserver:调用由@ try / @ catch包装-并不理想,但是苹果文档指定调用removeObserver时没有安全性:...如果该对象没有注册观察者,应用程序将崩溃。
奥斯丁,

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。

克里斯·凯恩(Chris Kane)可可框架,苹果

在我的情况下,可以知道上一次操作何时添加到队列中。因此,在添加了最后一个操作之后,我向队列添加了另一个优先级较低的操作,该操作只发送队列已被清空的通知。根据苹果的声明,这可以确保仅在所有操作完成后才发送单个通知。

如果以不允许检测最后一个(即不确定性)的方式添加操作,那么我认为您必须采用上述KVO方法,并添加了额外的保护逻辑以尝试检测是否进一步可以添加操作。

:)


嗨,您知道使用maxConcurrentOperationCount = 1的NSOperationQueue在队列中的每个操作结束时是否以及如何得到通知?
Sefran2 2011年

@fran:我希望操作完成后发布通知。这样,其他模块可以注册为观察者,并在每个模块完成时做出响应。如果您的@selector带有通知对象,则可以轻松地检索发布通知的对象,以防您需要有关刚刚完成的操作的更多详细信息。
软件

17

添加一个依赖于所有其他操作的NSOperation怎么样,这样它才能最后运行?


1
它可能会起作用,但这是一个重量级的解决方案,如果您需要向队列中添加新任务,将很难管理。
Kornel

这实际上是非常优雅的,我最喜欢的一个!你我的投票。
Yariv Nissim 2013年

1
就个人而言,这是我最喜欢的解决方案。您可以轻松地为依赖于所有其他操作的完成块创建一个简单的NSBlockOperation。
Puneet Sethi

您可能会遇到一个问题,即取消队列时未调用NSBlockOperation。因此,您需要进行自己的操作,该操作将在取消操作时产生错误,并使用错误参数调用一个块。
malhal '16

这是最好的答案!
捕手

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

这就是我的方法。

设置队列,并注册对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循环似乎很昂贵(如果您一次取消所有操作该怎么办?清理队列时不会获得二次性能?)
Kornel

不错,但是要小心线程,因为根据文档:“ ...与操作队列相关的KVO通知可能在任何线程中发生。” 也许,你需要在更新之前微调移动执行流程的主要操作队列
伊戈尔·瓦西列夫

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年

如果op1和op2在调用setCompletion之前完成,则此方法不起作用。
malhal 2016年

出色的答案,只有在开始所有操作的队列完成后调用完成块的1个警告。开始操作!=操作已完成。
萨奇布·沙特

嗯,老答案,但我打赌waitUntilFinished应该是YES
brandonscript

3

iOS 13.0开始,不建议使用operationCountoperation属性。自己跟踪队列中的操作数,并在完成所有操作后发出通知,就很简单。此示例也适用于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-我已用“ 操作”子菜单更新了答案。我在状态变量的didSet中使用了decrementOperationCount()。希望这可以帮助!
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开始)。你必须一直在寻找的最大OS X文档页面(developer.apple.com/Mac/library/documentation/Cocoa/Reference/...
尼克锻造

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

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

仅供参考,您可以使用GCD dispatch_groupswift 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版本是什么?
Nitesh Borad

它可以在Swift 3,iOS 8或更高版本中使用。
阿比吉斯(Abhijith)'17

0

您可以创建一个new 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包装在其他对象中。每当您将NSOperation排队时,就增加一个数字并启动一个线程。每当线程结束时,您就将该数字减一。我在考虑一种情况,您可以事先将所有内容排队,然后启动队列,因此您只需要一个等待线程。
pgb

0

如果您将此操作用作您的基类,则可以通过whenEmpty {} block给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 '18

我的糟糕……我做出了一个错误的结论,即上面的“ 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)")
})
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.