NSOperationQueue가 모든 작업을 완료하면 알림 받기


92

NSOperationQueuewaitUntilAllOperationsAreFinished있지만 동기식으로 기다리고 싶지 않습니다. 대기열이 완료되면 UI에서 진행률 표시기를 숨기고 싶습니다.

이를 수행하는 가장 좋은 방법은 무엇입니까?

NSOperations 에서 알림을 보낼 수 없습니다. 어떤 알림 이 마지막 [queue operations]일지 모르고 알림이 수신 될 때 아직 비어 있지 않을 수 있습니다 (또는 더 나쁘게-다시 채워짐).


신속한 3에서 GCD를 사용하고 있다면 이것을 확인하십시오. stackoverflow.com/a/44562935/1522584
Abhijith

답변:


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 회 이상 참조하지 않는 한 무시할 수있을 정도입니다 (매우 조잡한 추측).
Nick Forge

2
나쁜 KVO 사용으로 인해 반대표를 던지는 유혹을 받았습니다. 여기에 설명 된 적절한 사용법 : dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe

19
@NikolaiRuhe 맞습니다- operationCount동일한 NSOperationQueue객체 를 관찰하기 위해 KVO를 사용하는 클래스를 서브 클래 싱 할 때이 코드를 사용하면 잠재적으로 버그가 발생할 수 있으며,이 경우 컨텍스트 인수를 올바르게 사용해야합니다. 발생할 가능성은 낮지 만 확실히 가능합니다. (실제 문제 맞춤법은 스나크 + 링크를 추가하는 것보다 더 도움이된다)
닉 포지

6
여기서 흥미로운 아이디어를 찾았 습니다 . 이를 사용하여 NSOperationQueue를 하위 클래스로 만들고 대기열에 추가 된 각 작업에 종속 된 NSOperation 속성 'finalOpearation'을 추가했습니다. 그렇게하려면 addOperation :을 재정의해야했습니다. 또한 finalOperation이 완료되면 대리인에게 메시지를 보내는 프로토콜을 추가했습니다. 지금까지 일하고 있습니다.
pnizzle 2013 년

1
훨씬 낫다! 옵션이 지정되고 removeObserver : 호출이 @ try / @ catch로 래핑 될 때 가장 기쁠 것입니다. 이상적이지는 않지만 애플 문서에서는 removeObserver : ...를 호출 할 때 안전이 없다고 지정합니다. 개체에 관찰자 등록이없는 경우 응용 프로그램이 중단됩니다.
Austin

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입니까?"라는 질문에 대한 응답으로 Apple 개발자로부터이 설명을 찾았습니다. -

모든 작업의 ​​우선 순위가 같고 (작업이 대기열에 추가 된 후 변경되지 않음) 모든 작업이 작업 대기열에 들어갈 때 항상 isReady == YES이면 직렬 NSOperationQueue는 FIFO입니다.

Chris Kane Cocoa Frameworks, Apple

제 경우에는 마지막 작업이 대기열에 추가 된시기를 알 수 있습니다. 따라서 마지막 작업이 추가 된 후 우선 순위가 낮은 다른 작업을 대기열에 추가합니다.이 작업은 대기열이 비 었음을 알리는 알림 만 전송합니다. Apple의 진술에 따르면 모든 작업이 완료된 후에 만 ​​단일 통지 만 전송됩니다.

마지막 작업을 감지 할 수없는 방식 (즉, 비 결정적)으로 작업이 추가되는 경우 위에서 언급 한 KVO 접근 방식을 사용하고 추가 가드 로직을 추가하여 추가 여부를 감지해야한다고 생각합니다. 작업이 추가 될 수 있습니다.

:)


안녕하세요, maxConcurrentOperationCount = 1 인 NSOperationQueue를 사용하여 대기열의 각 작업이 종료 될 때 알림을받을 수 있는지 여부와 방법을 알고 있습니까?
Sefran2

@fran : 작업이 완료되면 알림을 게시하도록합니다. 이렇게하면 다른 모듈이 관찰자로 등록하고 각 모듈이 완료 될 때 응답 할 수 있습니다. @selector가 알림 개체를 사용하는 경우 방금 완료된 작업에 대한 추가 세부 정보가 필요한 경우 알림을 게시 한 개체를 쉽게 검색 할 수 있습니다.
소프트웨어는

17

다른 모든 것에 의존하는 NSOperation을 추가하여 마지막으로 실행하는 것은 어떻습니까?


1
작동 할 수도 있지만 무거운 솔루션이며 대기열에 새 작업을 추가해야하는 경우 관리하기가 어렵습니다.
Kornel

이것은 실제로 매우 우아하고 내가 가장 선호하는 것입니다! 당신은 내 투표입니다.
Yariv Nissim 2013

1
개인적으로 이것은 제가 가장 좋아하는 솔루션입니다. 다른 모든 작업에 의존하는 완료 블록에 대한 간단한 NSBlockOperation을 쉽게 만들 수 있습니다.
Puneet Sethi 16.02.22

큐가 취소 될 때 NSBlockOperation이 호출되지 않는 문제가 발생할 수 있습니다. 따라서 취소시 오류를 생성하고 오류 매개 변수로 블록을 호출하는 자체 작업을 만들어야합니다.
malhal

이것이 최고의 답변입니다!
사냥꾼

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

이 예에서 "spinner"는 UIActivityIndicatorView어떤 일이 일어나고 있음을 보여줍니다. 당연히 당신은 맞게 바꿀 수 있습니다 ...


2
for루프는 잠재적으로 비용이 많이 드는 것 같습니다 (모든 작업을 한 번에 취소하면 어떻게됩니까? 대기열이 정리 될 때 2 차 성능을 얻지 않습니까?)
Kornel

좋지만 스레드에주의하십시오. 문서에 따르면 "작업 대기열과 관련된 KVO 알림은 모든 스레드에서 발생할 수 있습니다." 아마도 스피너를 업데이트하기 전에 실행 흐름을 기본 작업 대기열로 이동해야 할 것입니다
Igor Vasilev

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-10-12

setCompletion이 호출되기 전에 op1 및 op2가 완료되면 작동하지 않습니다.
malhal

모든 작업을 시작하여 대기열이 완료되면 완료 블록이 호출된다는 단 한 가지주의 사항이 있습니다. 작업 시작! = 작업이 완료되었습니다.
Saqib Saud

흠 된 대답,하지만 난 거라고 베팅 waitUntilFinished해야YES
brandonscript

3

현재 아이폰 OS 13.0operationCount동작 특성은 사용되지 않습니다. 대기열의 작업 수를 직접 추적하고 작업 이 모두 완료되면 알림 을 실행하는 것도 간단합니다 . 이 예제는 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-나는 Operation 의 sublcass로 내 대답을 업데이트했습니다 . 내 상태 변수 의 didSet 내에서 decrementOperationCount ()를 사용 합니다. 도움이 되었기를 바랍니다!
Caleb Lindsey

2

operationCount큐 의 속성 을 관찰하기 위해 KVO를 사용하는 것은 어떻습니까? 그런 다음 대기열이 비었을 때와 비우기를 멈출 때에 대해 듣게됩니다. 진행률 표시기를 다루는 것은 다음과 같이 간단 할 수 있습니다.

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

이게 효과가 있었나요? 내 응용 프로그램 NSOperationQueue에서 3.1에서 키에 대해 KVO를 준수하지 않는다고 불평합니다 operationCount.
zoul 2010 년

나는 실제로이 솔루션을 앱에서 시도하지 않았습니다. OP가 그랬는지 말할 수 없습니다. 그러나 문서에는 작동 해야한다고 분명히 명시되어 있습니다. 버그 보고서를 제출하겠습니다. developer.apple.com/iphone/library/documentation/Cocoa/…
Sixten Otto

iPhone SDK의 NSOperationQueue에는 operationCount 속성이 없습니다 (적어도 3.1.3 이상). Max OS X 문서 페이지 ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Nick Forge

1
시간은 모든 상처를 치유하고 때로는 잘못된 대답을합니다. iOS 4부터는 operationCount속성이 있습니다.
Sixten Otto

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

2
그리고 대기열이 취소되면이 마지막 작업은 시작되지도 않습니다.
malhal 2016-04-12

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

참고로, 당신은 신속한 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 버전은 무엇입니까?
Nitesh Borad 2017

Swift 3, iOS 8 이상에서 사용할 수 있습니다.
Abhijith 2011

0

새를 만들 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을 대기열에 넣을 때마다 번호가 증가하고 스레드가 시작됩니다. 스레드가 끝날 때마다 그 수를 1 씩 감소시킵니다. 모든 것을 미리 대기열에 넣은 다음 대기열을 시작할 수있는 시나리오를 생각하고 있었으므로 대기중인 스레드가 하나만 필요합니다.
pgb 2009-06-27

0

작업 을 기본 클래스로 사용하면 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

@Dale 링크를 클릭하면 모든 것이 설명 된 github 페이지로 이동합니다. 내가 올바르게 기억한다면 Foundation의 OperationQueue가 여전히 NSOperationQueue라고 불릴 때 답변이 작성되었습니다. 모호함이 적었을 것입니다.
user1244109

내 잘못은 위의 "OperationQueue"가 Swift 4의 "OperationQueue"라는 잘못된 결론을 내 렸습니다.
Dale

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

여기에 결합을 사용하여 솔루션을 찾고 있다면 내 자신의 상태 개체를 듣고 끝났습니다.

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

sub = self.$state.sink(receiveValue: { (state) in
 print("state updated: \(state)")
})
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.