認識NSOperation

最近在研究NSOperation+NSOperationQueue vs GCD的部份; 本篇是關於NSOperation + NSOperationQueue的一些介紹

關於NSOperation, 基本上是用來操作/執行一個單一的任務, 如果你的任務不複雜, 其實可以使用NSInvocationOperation或是NSBlockOperation直接使用;關於NSOperation又可以分成並發(concurrent, 並連)或非並發(non-concurrent, 串連), 這篇會稍微介紹一下在不使用NSOperationQueue如何成為一個concurrent(asynchrouons)的operation.

這篇會提到的內容

  • 如何建立Subclass of Operation
  • 建立concurrent Operation
  • Operation在Queue的用法

可能還會提到一點點的NSThread&NSRunLoop

如何建立Subclass of NSOperation

因為NSOpertaion是一個抽象的Class, 所以在繼承的時候, 會需要去實作一些內容; 要實作的內容又因為Operation是concurrent或是non-concurrent有所不同.

non-concurrent

建立non-concurrent的operation比較簡單, 只要在建立的class去實作下面的method的就可以了

non-concurrent operation need implement method
1
- (void)main;

在這個method中完成你要做的事情, 就是一個non-concurrent operation了.

concurrent

在實作一個concurrent operation相對來說複雜了一點, 你最少需要實作(override)下列幾個methods.

concurrent operation need implement methods
1
2
3
4
- (void)start;
- (BOOL)isConcurrent;
- (BOOL)isExecuting;
- (BOOL)isFinished;

其中下面兩mehtod改變數值時個需要實作KVO notifications.

  • isExecuting
  • isFinished

start中, 你必須要去實現異步(asynchronous)的方式, 你會需要產生一個thread讓operation的任務執行在這個thread中; 這邊同時還要注意的是, 不可以在這邊使用[super start]; 以及在執行之前是否這個Operation是否已經被Cancel(isFinished)的狀況.

建立concurrent Operation

在上面我們已經了解了建立一個 Subclass of Operation需要實作哪些內容; 接下來直接進入如何建立自己的, 那麼我們開始建立一個新的Class MyOperation 並繼承 NSOperation, 並且有一個建立instance的Mehtod, 可以傳入一個block action. 下面我們先大致定義一些內容, 方便之後的實作.

Create MyOpertation Class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MyOperation.h
typedef void (^MyOperationAction)(void);
@interface MyOperation : NSOperation
- (id)initWithAction:(MyOperationAction)action;
@end
// MyOperation.m

typedef NS_ENUM(NSInteger, MyOperationState) {
    MyOperationReadyState = 1,
    MyOperationExecutingState,
    MyOperationFinishedState
};
@interface MyOperation () {
    MyOperationAction _action;
    MyOperationState _state;
}
@property (nonatomic, copy) MyOperationAction action;
@property (nonatomic, assign) MyOperationState state;

在.m中, 建立了一個列舉來代表Operation的執行狀態, 接下來我們覆寫幾個應該要實作的method

Override Methods
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma mark - Override
- (BOOL)isConcurrent {
    return YES;
}

- (BOOL)isExecuting {
    return self.state == MyOperationExecutingState;
}

- (BOOL)isFinished {
    return self.state == MyOperationFinishedState;
}

- (void)start {
    // 在這邊坐些什麼吧
}

在上面有提到, 如果要讓Operation實現Concurrent我們就必須在在start中去建立一個Thread, 並且讓他的任務在這個Thread中執行; 因此我們將建立一個singleton thread來讓MyOperation使用

Create Singleton Thread
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ (void)keepThreadAlive {
    do {
        @autoreleasepool {
            [[NSRunLoop currentRunLoop] run];
        }
    } while (YES);
}

+ (NSThread*)threadForMyOperation {
    static NSThread* _threadInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _threadInstance = [[NSThread alloc] initWithTarget:self
                                           selector:@selector(keepThreadAlive)
                                             object:nil];
        _threadInstance.name = @"MyOperation.Thread";
        [_threadInstance start];
    });
    return _threadInstance;
}

上面有兩個method, 其中keepThreadAlive就如同Method的名字一樣, 是為了要讓Thread可以持續的運作, 不會在還沒做完事情, thread就結束了. 裡面的作法就是給他一個無限的loop, 去執行NSRunLooprunmehtod. 讓這個thread成為NSRunLoop的一個input source.

這樣子的作法會讓這個thread, 一直存活直到user把app關閉才會結束.

接下來我們回到start

run the task with MyThread for MyOperation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)start {
    if([self isReady]) {
        self.state = MyOperationExecutingState;
        [self performSelector:@selector(operationDidStart)
                     onThread:[[self class] threadForMyOperation]
                   withObject:nil
                waitUntilDone:NO];
    }
}

- (void)operationDidStart {
    if(self.isCancelled) {
        self.state = MyOperationFinishedState;
    } else {
        NSLog(@"Operation is running %@ thread", [NSThread currentThread]);
        self.action();
        self.state = MyOperationFinishedState;
    }
}

到這邊基本上已經完成concurrent operation的實作了, 我們先來測試一下跑起來的情況; 這邊我用一個BlockOperation跟MyOperation一起執行, 我把 MyOperation放在blockOperation前執行~

test MyOperation
1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)runOperations {
    MyOperation* myOperation = [[MyOperation alloc] initWithAction:^{
        NSLog(@"this is MyOperation");
    }];

    NSBlockOperation* blockOperation = [[NSBlockOperation alloc] init];
    [blockOperation addExecutionBlock:^{
        NSLog(@"this is block Operation");
    }];

    [myOperation start];
    [blockOperation start];
}

從上圖顯示的Log, 兩個Operation第一個Log的時間是相同的, 所以不算上延遲的話 … 應該已經達到我們想要的asynchronous效果, 那麼接下來在來看看在start中我們有使用到[self isReady], 另外在operationDidStart中也有去判斷operation是否已經cancel的狀態, 所以我們會需要對isReady以及cancel這兩個method做一些調整

add property for isCancelled and override isReady , cancel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// MyOperation.m

@interface MyOperation () {
    MyOperationAction _action;
    MyOperationState _state;
    BOOL _cancel;
}
@property (nonatomic, copy) MyOperationAction action;
@property (nonatomic, assign) MyOperationState state;
@property (nonatomic, readonly, getter = isCancelled) BOOL cancel;
@end

// override method

- (BOOL)isReady {
    return self.state == MyOperationReadyState;
}

- (void)cancel {
  _cancel = YES;
  // 如果你的Operation是執行一些資料處理 or request, 可以做一些其他的處理
}

到這邊我們把需要用到的method都有實作到了, 不過前面我們有提到isExecutingisFinished是需要實作KVO Nofifications的, 在文件中你可以看到需要generate KVO notifications的property大該有下列幾個

  • isCancelled - read-only property
  • isConcurrent - read-only property
  • isExecuting - read-only property
  • isFinished - read-only property
  • isReady - read-only property
  • dependencies - read-only property
  • queuePriority - readable and writable property
  • completionBlock - readable and writable property

其中粗體的部份是在MyOperation會變動的, 所以我們必須在變動的時候送出KVO Notification, 因此我們再稍微調整一下, 在有修改到state幾個地方跟cancel都加上動作

add kvo notifications
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- (void)cancel {
    [self willChangeValueForKey:@"isCancelled"];
  _cancelled = YES;
    [self didChangeValueForKey:@"isCancelled"];
}

- (void)start {
    if([self isReady]) {

        [self willChangeValueForKey:@"isExecuting"];
        [self willChangeValueForKey:@"isReady"];
        _state = MyOperationExecutingState;
        [self didChangeValueForKey:@"isReady"];
        [self didChangeValueForKey:@"isExecuting"];

        [self performSelector:@selector(operationDidStart)
                     onThread:[[self class] threadForMyOperation]
                   withObject:nil
                waitUntilDone:NO];
    }
}

- (void)operationDidStart {
    if(self.isCancelled) {
        [self willChangeValueForKey:@"isFinished"];
        [self willChangeValueForKey:@"isCancelled"];
        _state = MyOperationFinishedState;
        [self didChangeValueForKey:@"isCancelled"];
        [self didChangeValueForKey:@"isFinished"];
    } else {
        NSLog(@"Operation is running %@ thread", [NSThread currentThread]);
        self.action();
        [self willChangeValueForKey:@"isFinished"];
        [self willChangeValueForKey:@"isExecuting"];
        _state = MyOperationFinishedState;
        [self didChangeValueForKey:@"isExecuting"];
        [self didChangeValueForKey:@"isFinished"];
    }
}

這樣, 就完成了一個簡易的Operation了~ 當然在不同的情況之下還是有一些部分需要做調整, 例如使用OperationQueue的時候, 你為Operation加上dependent operation, 就必須要讓isReady return NO, 否則queue可能就會判斷operation isReady=YES, 就直接去執行, 這樣造成結果或執行上的錯誤.

Operation在Queue的用法

在前面有講接一些該如何去實作一個Operation; 當然提到Operation通常都不會忘掉OperationQueue, 使用Queue, 除了可以讓non-concurrent operation達到concurrent的效果外, 也可以讓Operation去等待某些Operation完成後再去執行(替operation加上dependent operation), 而且比起每次都自行去建立Thread並且在用完後自行回收, OperationQueue使用起來更加的方便更有效率.

在使用的時候, 要讓non-concurrent operation在queue中可以以concurrent的方式去執行, 你必須要建立一個NSOperationQueue實體, 如果使用[NSOperationQueue mainQueue]所取得的queue, 除非operation有特別處理過, 不然都會在main thread中執行, 不過依然會等待上一個operation完成後才會去執行; 以下面的範例, 我們來看看輸出的結果會如何:

example for NSOperationQueue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (void)runOperationsatCustomQueue {
    NSBlockOperation* blockOperation01 = [[NSBlockOperation alloc] init];
    [blockOperation01 addExecutionBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
        NSLog(@"this is no.1 block Operation");
    }];

    NSBlockOperation* blockOperation02 = [[NSBlockOperation alloc] init];
    [blockOperation02 addExecutionBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
        NSLog(@"this is no.2 block Operation, start sleep 1 second");
        sleep(1);
        NSLog(@"no.2 wake up.");
    }];

    NSBlockOperation* blockOperation03 = [[NSBlockOperation alloc] init];
    [blockOperation03 addExecutionBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
        NSLog(@"this is no.3 block Operation, no.2 operation is completed, start task");
    }];

    [blockOperation03 addDependency:blockOperation02];

    NSOperationQueue* queue = [[NSOperationQueue alloc] init];
    [queue setSuspended:YES];
    [queue setMaxConcurrentOperationCount:5];
    [queue addOperation:blockOperation01];
    [queue addOperation:blockOperation02];
    [queue addOperation:blockOperation03];
    [queue setSuspended:NO];
}

從上面的結果, 你可以看到queue幫我們建立了三個thread分別給operation01、02跟03使用(有時候02跟03可能會出現公用一個thread的狀況), 你並不需要去特別管理thread, queue會自己幫你完成; 在上面的程式碼中, 我設定了queue一次最多可以執行五個operation, 然後operation03必須等operation02完成後, 才會接著執行, 因此圖片中顯示的結果是operation01跟02是一起執行, 接著operation03才執行.

那如果這時候把上面使用的MyOperation會發生什麼事勒, 我們來試試看~ 下面改一下程式碼

add MyOperation to the queue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MyOperation* myOperation01 = [[MyOperation alloc] initWithAction:^{
  NSLog(@"this is 01 MyOperation");
}];

MyOperation* myOperation02 = [[MyOperation alloc] initWithAction:^{
  NSLog(@"this is 02 MyOperation");
}];

NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue setSuspended:YES];
[queue setMaxConcurrentOperationCount:5];
[queue addOperation:myOperation01];
[queue addOperation:myOperation02];
[queue addOperation:blockOperation01];
...

基本上bopt01、02跟myopt01(myopt02)會是concurrent, 而myopt01跟02之間則是non-concurrent必須要其中一個完成後才會繼續執行(共用同一個thread).

那麼, 有沒有辦法讓Operation(ex:MyOperation)自己執行的時候是concurrent, 然後在queue中執行的時候也是concurrent的方式?

答案我想是有的, 多一個Mehtodasychronous跟變數去判斷是不是asynchronous啟動opertaion, 是的話就給他一個thread去執行任務, 如果直接執行start, 就會是non-concurrent operation; 這樣在使用queue的時候, 因為queue會直接去執行start, 就可以直接幫operation建立一個thread達到concurrent的目的. (isConcurrent = YES or NO, 好像不是主要的判斷方式)

最後

打完後發現感覺是打給自己看的XD, 都是程式碼~; 不過也把它留存當記錄囉, 如果有幫助到其他人也很棒. 有看到的人如果發現錯誤也麻煩幫忙指正一下, 感謝.

最後附上參考網址跟一些stackoverflow的內容:

Comments