最近在研究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
在這個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.
在start
中, 你必須要去實現異步(asynchronous)的方式, 你會需要產生一個thread讓operation的任務執行在這個thread中; 這邊同時還要注意的是, 不可以在這邊使用[super start]
; 以及在執行之前是否這個Operation是否已經被Cancel(isFinished)的狀況.
建立concurrent Operation
在上面我們已經了解了建立一個 Subclass of Operation需要實作哪些內容; 接下來直接進入如何建立自己的, 那麼我們開始建立一個新的ClassMyOperation
並繼承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, 去執行NSRunLoop
的run
mehtod. 讓這個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都有實作到了, 不過前面我們有提到isExecuting
、isFinished
是需要實作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 returnNO
, 否則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的內容: