【问题标题】:Get notification when NSOperationQueue finishes all tasks当 NSOperationQueue 完成所有任务时获取通知
【发布时间】:2010-11-06 03:31:53
【问题描述】:

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

最好的方法是什么?

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

【问题讨论】:

标签: iphone asynchronous queue notifications nsoperation


【解决方案1】:

使用 KVO 观察队列的 operations 属性,然后您可以通过检查 [queue.operations count] == 0 来判断队列是否已完成。

在您正在执行 KVO 的文件的某处,像这样声明 KVO 的上下文 (more info):

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 中仍然有效,因此对于向后兼容仍然有用。

【讨论】:

  • 我认为你应该使用属性访问器,因为它提供了面向未来的封装(如果你决定延迟初始化队列)。通过其 ivar 直接访问属性可能被认为是过早的优化,但这实际上取决于确切的上下文。通过 ivar 直接访问属性所节省的时间通常可以忽略不计,除非您每秒引用该属性超过 100-1000 次(作为一个非常粗略的猜测)。
  • 由于 KVO 使用不当,很想投反对票。此处描述的正确用法:dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
  • @NikolaiRuhe 你是对的 - 在子类化一个本身使用 KVO 的类时使用此代码来观察同一 operationCount 对象上的 operationCount 可能会导致错误,在这种情况下你需要正确使用上下文参数。这不太可能发生,但绝对有可能。 (拼出实际问题比添加 snark + 链接更有帮助)
  • 发现了一个有趣的想法here。我用它来继承 NSOperationQueue,添加了一个 NSOperation 属性“finalOperation”,它被设置为添加到队列中的每个操作的依赖项。显然必须重写 addOperation: 才能这样做。还添加了一个在 finalOperation 完成时向委托发送消息的协议。到目前为止一直在工作。
  • 好多了!当指定选项时,我会很高兴,并且 removeObserver: 调用由 @try/@catch 包装 - 这并不理想,但苹果文档指定调用 removeObserver: ... if 时没有安全性该对象没有观察者注册,应用程序将崩溃。
【解决方案2】:

如果您期望(或渴望)符合这种行为的东西:

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)之后,并且只有在它们全部完成之后。

谷歌搜索我从一位 Apple 开发人员那里找到了这个声明,以回答“是串行 NSoperationQueue FIFO 吗?”这个问题。 --

如果所有操作具有相同的优先级(在 操作被添加到队列中)并且所有操作总是 - 当它们被放入操作队列时,isReady==YES,然后是串行 NSOperationQueue 是先进先出的。

克里斯·凯恩 可可框架,苹果

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

如果以不允许检测最后一个操作的方式添加操作(即非确定性),那么我认为您必须使用上面提到的 KVO 方法,并添加额外的保护逻辑来尝试检测是否可以添加更多操作。

:)

【讨论】:

  • 您好,您知道是否以及如何通过使用 maxConcurrentOperationCount=1 的 NSOperationQueue 在队列中的每个操作结束时收到通知?
  • @fran:我会让操作在完成后发布通知。这样其他模块可以注册为观察者,并在每个完成时做出响应。如果您的 @selector 采用通知对象,您可以轻松检索发布通知的对象,以防您需要有关刚刚完成的操作的更多详细信息。
【解决方案3】:

添加一个依赖于所有其他的 NSOperation 以使其最后运行怎么样?

【讨论】:

  • 它可能有效,但它是一个重量级的解决方案,如果您需要将新任务添加到队列中,管理起来会很痛苦。
  • 这实际上非常优雅,也是我最喜欢的!你是我的一票。
  • 就个人而言,这是我最喜欢的解决方案。您可以轻松地为依赖于所有其他操作的完成块创建一个简单的 NSBlockOperation。
  • 您可能会遇到取消队列时未调用 NSBlockOperation 的问题。因此,您需要进行自己的操作,在取消时创建错误并使用错误参数调用块。
  • 这是最好的答案!
【解决方案4】:

另一种方法是使用 GCD。请参阅this 作为参考。

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】:

    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 - 我已经用 Operation 的子类更新了我的答案。我在 state 变量的 didSet 中使用 decrementOperationCount()。希望这会有所帮助!
    • 这仅在将 maxConcurrentOperationCount 设置为 1 时有效,因为如果它们都同时发生,则 numberOfOperations 理论上可能会错过递减 inovkation,因为每个操作都发生在不同的线程上,numberOfOperations 变量是不是线程安全的。
    【解决方案6】:

    我就是这样做的。

    设置队列,并注册操作属性中的更改:

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

    在这个例子中,“spinner”是一个UIActivityIndicatorView,表示正在发生一些事情。显然你可以改变以适应...

    【讨论】:

    • for 循环似乎可能很昂贵(如果您一次取消所有操作会怎样?在清理队列时不会获得二次性能吗?)
    • 不错,但要小心线程,因为根据文档:“...与操作队列相关的 KVO 通知可能发生在任何线程中。”可能,您需要在更新微调器之前将执行流移动到主操作队列
    【解决方案7】:

    我正在使用一个类别来执行此操作。

    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 的生命周期内,可以多次进入空状态。
    • 如果 op1 和 op2 在调用 setCompletion 之前完成,这将不起作用。
    • 很好的答案,只有 1 个警告,当队列完成并开始所有操作时,完成块会被调用。开始操作!= 操作完成。
    • 嗯,老答案,但我敢打赌 waitUntilFinished 应该是 YES
    【解决方案8】:

    使用KVO观察队列的operationCount属性怎么样?然后,当队列变空以及停止为空时,您会听到它。处理进度指示器可能就像执行以下操作一样简单:

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

    【讨论】:

    • 这对你有用吗?在我的应用程序中,来自 3.1 的 NSOperationQueue 抱怨它与密钥 operationCount 不兼容。
    • 我实际上并没有在应用程序中尝试过这个解决方案,没有。不能说OP是否做到了。但是文档清楚地表明它应该起作用。我会提交一份错误报告。 developer.apple.com/iphone/library/documentation/Cocoa/…
    • iPhone SDK 中的 NSOperationQueue 上没有 operationCount 属性(至少从 3.1.3 开始没有)。您一定一直在查看 Max OS X 文档页面 (developer.apple.com/Mac/library/documentation/Cocoa/Reference/…)
    • 时间可以治愈所有的伤口……有时也会有错误的答案。从 iOS 4 开始,operationCount 属性存在。
    【解决方案9】:

    添加最后一个操作,如:

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

    【讨论】:

    • 当任务同时执行时,这是错误的方法。
    • 当队列被取消时,最后一个操作甚至都没有开始。
    【解决方案10】:

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

    【讨论】:

      【解决方案11】:

      仅供参考,您可以使用 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 版本是多少?
      • Swift 3、iOS 8 或更高版本可用。
      【解决方案12】:

      你可以创建一个新的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"];
      }
      

      【讨论】:

      • 创建线程只是为了让它进入睡眠状态似乎有点愚蠢。
      • 我同意。尽管如此,我还是找不到其他方法。
      • 如何确保只有一个线程在等待?我考虑过 flag,但需要保护它免受竞争条件的影响,而且我最终使用了太多的 NSLock 来满足我的口味。
      • 我认为您可以将 NSOperationQueue 包装在其他对象中。每当您排队 NSOperation 时,您都会增加一个数字并启动一个线程。每当线程结束时,您将该数字减一。我正在考虑一种场景,您可以预先将所有内容排队,然后启动队列,因此您只需要一个等待线程。
      【解决方案13】:

      如果你使用这个Operation 作为你的基类,你可以将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 如果您单击该链接,它将带您到一个 github 页面,其中解释了所有内容。如果我没记错的话,答案是在 Foundation 的 OperationQueue 仍被称为 NSOperationQueue 时写的;所以可能会减少歧义。
      • 我的错...我得出了错误的结论,即上面的“OperationQueue”是 Swift 4 的“OperationQueue”。
      【解决方案14】:

      没有 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)
          }
      }
      

      【讨论】:

        【解决方案15】:

        如果您来这里是在寻找使用 combine 的解决方案 - 我最终只是在听我自己的状态对象。

        @Published var state: OperationState = .ready
        var sub: Any?
        
        sub = self.$state.sink(receiveValue: { (state) in
         print("state updated: \(state)")
        })
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-09-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多