1、簡介
NSOperation是蘋果提供給我們的一套多線程解決方案。實際上NSOperation是基于GCD更高一層的封裝,但是比GCD更簡單易用、代碼可讀性也更高。
NSOperation需要配合NSOperationQueue來實現多線程。因為默認情況下,NSOperation單獨使用時系統同步執行操作,并沒有開辟新線程的能力,只有配合NSOperationQueue才能實現異步執行。
因為NSOperation是基于GCD的,那么使用起來也和GCD差不多,其中,NSOperation相當于GCD中的任務,而NSOperationQueue則相當于GCD中的隊列。NSOperation實現多線程的使用步驟分為三步:
- 創建任務:先將需要執行的操作封裝到一個NSOperation對象中。
- 創建隊列:創建NSOperationQueue對象。
- 將任務加入到隊列中:然后將NSOperation對象添加到NSOperationQueue中。
之后呢,系統就會自動將NSOperationQueue中的NSOperation取出來,在新線程中執行操作。
下面我們來學習下NSOperation和NSOperationQueue的基本使用。
2、NSOperation和NSOperationQueue的基本使用
1、創建任務
NSOperation是個抽象類,并不能封裝任務。我們只有使用它的子類來封裝任務。我們有三種方式來封裝任務。
- 使用子類NSInvocationOperation
- 使用子類NSBlockOperation
- 定義繼承自NSOperation的子類,通過實現內部相應的方法來封裝任務。
在不使用NSOperationQueue,單獨使用NSOperation的情況下系統同步執行操作,下面我們學習以下任務的三種創建方式。
1、使用子類NSInvocationOperation
// 1.創建NSInvocationOperation對象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
// 2.調用start方法開始執行操作
[op start];
- (void)run
{
NSLog(@"------%@", [NSThread currentThread]);
}
輸出結果:
2017-11-20 10:41:16.521032+0800 test[5023:190051] ------<NSThread: 0x6040000662c0>{number = 1, name = main}
從中可以看到,在沒有使用NSOperationQueue、單獨使用NSInvocationOperation的情況下,NSInvocationOperation在主線程執行操作,并沒有開啟新線程。
下邊再來看看NSBlockOperation。
2、使用子類NSBlockOperation
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
// 在主線程
NSLog(@"------%@", [NSThread currentThread]);
}];
[op start];
輸出結果:
2017-11-20 10:43:29.704281+0800 test[5145:202303] ------<NSThread: 0x60400006bac0>{number = 1, name = main}
我們同樣可以看到,在沒有使用NSOperationQueue、單獨使用NSBlockOperation的情況下,NSBlockOperation也是在主線程執行操作,并沒有開啟新線程。
但是,NSBlockOperation還提供了一個方法addExecutionBlock:
,通過addExecutionBlock:
就可以為NSBlockOperation添加額外的操作,這些操作就會并發執行。
- (void)blockOperation
{
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1------%@", [NSThread currentThread]);
}];
// 添加額外的任務
[op addExecutionBlock:^{
NSLog(@"2------%@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"3------%@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"4------%@", [NSThread currentThread]);
}];
[op start];
}
輸出結果:
2017-11-20 10:46:12.639798+0800 test[5279:215979] 1------<NSThread: 0x6000004624c0>{number = 3, name = (null)}
2017-11-20 10:46:12.639797+0800 test[5279:215842] 4------<NSThread: 0x60400007ac80>{number = 1, name = main}
2017-11-20 10:46:12.639797+0800 test[5279:215977] 2------<NSThread: 0x600000462500>{number = 5, name = (null)}
2017-11-20 10:46:12.639829+0800 test[5279:215976] 3------<NSThread: 0x604000464940>{number = 4, name = (null)}
可以看出,blockOperationWithBlock:
方法和addExecutionBlock:
方法所添加的操作是在多條線程中執行的。
3、定義繼承自NSOperation的子類
先定義一個繼承自NSOperation的子類,重寫main方法
YSCOperation.h
#import <Foundation/Foundation.h>
@interface YSCOperation : NSOperation
@end
YSCOperation.m
#import "YSCOperation.h"
@implementation YSCOperation
/**
* 需要執行的任務
*/
- (void)main
{
for (int i = 0; i < 2; ++i) {
NSLog(@"1-----%@",[NSThread currentThread]);
}
}
@end
然后使用的時候導入頭文件YSCOperation.h。
// 創建YSCOperation
YSCOperation *op1 = [[YSCOperation alloc] init];
[op1 start];
輸出結果:
2017-11-20 10:56:40.268238+0800 test[5904:271380] 1-----<NSThread: 0x604000069300>{number = 1, name = main}
2017-11-20 10:56:40.268409+0800 test[5904:271380] 1-----<NSThread: 0x604000069300>{number = 1, name = main}
可以看出:在沒有使用NSOperationQueue、單獨使用自定義子類的情況下,是在主線程執行操作,并沒有開啟新線程。
下邊我們簡單講講NSOperationQueue的創建。
2、創建隊列
和GCD中的并發隊列、串行隊列略有不同的是:NSOperationQueue一共有兩種隊列:主隊列、其他隊列。其中其他隊列同時包含了串行、并發功能。下邊是主隊列、其他隊列的基本創建方法和特點。
- 主隊列
凡是添加到主隊列中的任務(NSOperation),都會放到主線程中執行
NSOperationQueue *queue = [NSOperationQueue mainQueue];
- 其他隊列(非主隊列)
添加到這種隊列中的任務(NSOperation),就會自動放到子線程中執行
同時包含了:串行、并發功能
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
3、將任務加入到隊列中
前邊說了,NSOperation需要配合NSOperationQueue來實現多線程。
那么我們需要將創建好的任務加入到隊列中去。總共有兩種方法
-
- (void)addOperation:(NSOperation *)op;
需要先創建任務,再將創建好的任務加入到創建好的隊列中去
- (void)addOperationToQueue
{
// 1.創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2. 創建操作
// 創建NSInvocationOperation
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
// 創建NSBlockOperation
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; ++i) {
NSLog(@"1-----%@", [NSThread currentThread]);
}
}];
// 3. 添加操作到隊列中:addOperation:
[queue addOperation:op1]; // [op1 start]
[queue addOperation:op2]; // [op2 start]
}
- (void)run
{
for (int i = 0; i < 2; ++i) {
NSLog(@"2-----%@", [NSThread currentThread]);
}
}
輸出結果:
2017-11-20 11:06:47.458543+0800 test[6376:322141] 1-----<NSThread: 0x60400046c180>{number = 3, name = (null)}
2017-11-20 11:06:47.458580+0800 test[6376:322142] 2-----<NSThread: 0x60400046c200>{number = 4, name = (null)}
2017-11-20 11:06:47.458878+0800 test[6376:322142] 2-----<NSThread: 0x60400046c200>{number = 4, name = (null)}
2017-11-20 11:06:47.458885+0800 test[6376:322141] 1-----<NSThread: 0x60400046c180>{number = 3, name = (null)}
可以看出:NSInvocationOperation和NSOperationQueue結合后能夠開啟新線程,進行并發執行。NSBlockOperation和NSOperationQueue也能夠開啟新線程,進行并發執行。
-
- (void)addOperationWithBlock:(void (^)(void))block;
無需先創建任務,在block中添加任務,直接將任務block加入到隊列中。
- (void)addOperationWithBlockToQueue
{
// 1. 創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2. 添加操作到隊列中:addOperationWithBlock:
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; ++i) {
NSLog(@"-----%@", [NSThread currentThread]);
}
}];
}
輸出結果:
2017-11-20 11:17:46.614125+0800 test[6880:376408] -----<NSThread: 0x60000026e500>{number = 3, name = (null)}
2017-11-20 11:17:46.614348+0800 test[6880:376408] -----<NSThread: 0x60000026e500>{number = 3, name = (null)}
可以看出addOperationWithBlock:和NSOperationQueue能夠開啟新線程,進行并發執行。
3、控制串行執行和并行執行的關鍵
之前我們說過,NSOperationQueue創建的其他隊列同時具有串行、并發功能,上邊我們演示了并發功能,那么他的串行功能是如何實現的?
這里有個關鍵參數maxConcurrentOperationCount
,叫做最大并發數。
- 最大并發數:
maxConcurrentOperationCount
-
maxConcurrentOperationCount
默認情況下為-1,表示不進行限制,默認為并發執行。 - 當
maxConcurrentOperationCount
為1時,進行串行執行。 - 當
maxConcurrentOperationCount
大于1時,進行并發執行,當然這個值不應超過系統限制,即使自己設置一個很大的值,系統也會自動調整。
- (void)opetationQueue
{
// 創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 設置最大并發操作數
// queue.maxConcurrentOperationCount = 2;
queue.maxConcurrentOperationCount = 1; // 就變成了串行隊列
// 添加操作
[queue addOperationWithBlock:^{
NSLog(@"1-----%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:0.01];
}];
[queue addOperationWithBlock:^{
NSLog(@"2-----%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:0.01];
}];
[queue addOperationWithBlock:^{
NSLog(@"3-----%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:0.01];
}];
[queue addOperationWithBlock:^{
NSLog(@"4-----%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:0.01];
}];
[queue addOperationWithBlock:^{
NSLog(@"5-----%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:0.01];
}];
[queue addOperationWithBlock:^{
NSLog(@"6-----%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:0.01];
}];
}
最大并發數為1輸出結果:
2017-11-20 11:23:03.110467+0800 test[7122:398971] 1-----<NSThread: 0x600000464c00>{number = 3, name = (null)}
2017-11-20 11:23:03.120945+0800 test[7122:398973] 2-----<NSThread: 0x6040004614c0>{number = 4, name = (null)}
2017-11-20 11:23:03.131556+0800 test[7122:398971] 3-----<NSThread: 0x600000464c00>{number = 3, name = (null)}
2017-11-20 11:23:03.144559+0800 test[7122:398973] 4-----<NSThread: 0x6040004614c0>{number = 4, name = (null)}
2017-11-20 11:23:03.155188+0800 test[7122:398971] 5-----<NSThread: 0x600000464c00>{number = 3, name = (null)}
2017-11-20 11:23:03.165532+0800 test[7122:398973] 6-----<NSThread: 0x6040004614c0>{number = 4, name = (null)}最大并發數為2輸出結果:
2017-11-20 11:23:55.953623+0800 test[7179:404017] 2-----<NSThread: 0x60000027a940>{number = 4, name = (null)}
2017-11-20 11:23:55.953644+0800 test[7179:404018] 1-----<NSThread: 0x60000027aa00>{number = 3, name = (null)}
2017-11-20 11:23:55.964036+0800 test[7179:404015] 3-----<NSThread: 0x60400007fc80>{number = 5, name = (null)}
2017-11-20 11:23:55.964046+0800 test[7179:404016] 4-----<NSThread: 0x600000277240>{number = 6, name = (null)}
2017-11-20 11:23:55.974361+0800 test[7179:404018] 5-----<NSThread: 0x60000027aa00>{number = 3, name = (null)}
2017-11-20 11:23:55.974361+0800 test[7179:404017] 6-----<NSThread: 0x60000027a940>{number = 4, name = (null)}
可以看出:當最大并發數為1時,任務是按順序串行執行的。當最大并發數為2時,任務是并發執行的。而且開啟線程數量是由系統決定的,不需要我們來管理。這樣看來,是不是比GCD還要簡單了許多?
4、操作依賴
NSOperation和NSOperationQueue最吸引人的地方是它能添加操作之間的依賴關系。比如說有A、B兩個操作,其中A執行完操作,B才能執行操作,那么就需要讓B依賴于A。具體如下:
- (void)addDependency
{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1-----%@", [NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2-----%@", [NSThread currentThread]);
}];
[op2 addDependency:op1]; // 讓op2 依賴于 op1,則先執行op1,在執行op2
[queue addOperation:op1];
[queue addOperation:op2];
}
輸出結果:
2017-11-20 11:27:36.191658+0800 test[7363:423406] 1-----<NSThread: 0x600000261740>{number = 3, name = (null)}
2017-11-20 11:27:36.192122+0800 test[7363:423409] 2-----<NSThread: 0x600000261a80>{number = 4, name = (null)}
可以看到,無論運行幾次,其結果都是op1先執行,op2后執行。
5、一些其他方法
- -(void)cancel; NSOperation提供的方法,可取消單個操作
- -(void)cancelAllOperations; NSOperationQueue提供的方法,可以取消隊列的所有操作
- -(void)setSuspended:(BOOL)b; 可設置任務的暫停和恢復,YES代表暫停隊列,NO代表恢復隊列
- -(BOOL)isSuspended; 判斷暫停狀態
- 注意:
1.這里的暫停和取消并不代表可以將當前的操作立即取消,而是當當前的操作執行完畢之后不再執行新的操作。
2.暫停和取消的區別就在于:暫停操作之后還可以恢復操作,繼續向下執行;而取消操作之后,所有的操作就清空了,無法再接著執行剩下的操作。
6、NSOperation優先級
GCD中,任務(block)是沒有優先級的,而隊列具有優先級。和GCD相反,我們一般考慮 NSOperation 的優先級。
NSOperation 有一個NSOperationQueuePriority 枚舉類型的屬性 queuePriority。
public enum NSOperationQueuePriority : Int {
case VeryLow
case Low
case Normal
case High
case VeryHigh
}
需要注意的是,NSOperationQueue 也不能完全保證優先級高的任務一定先執行。
queuePriority默認值是NSOperationQueuePriorityNormal。根據實際需要我們可以通過調用queuePriority的setter方法修改某個操作的優先級。
NSBlockOperation *blkop1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"執行blkop1");
}];
NSBlockOperation *blkop2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"執行blkop2");
}];
// 設置操作優先級
blkop1.queuePriority = NSOperationQueuePriorityLow;
blkop2.queuePriority = NSOperationQueuePriorityVeryHigh;
NSLog(@"blkop1 == %@",blkop1);
NSLog(@"blkop2 == %@",blkop2);
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 操作添加到隊列
[queue addOperation:blkop1];
[queue addOperation:blkop2];
NSLog(@"%@",[queue operations]);
for (NSOperation *op in [queue operations]) {
NSLog(@"op == %@",op);
}
輸出結果:
2017-02-12 19:36:01.149 NSOperation[1712:177976] blkop1 == <NSBlockOperation: 0x608000044440>
2017-02-12 19:36:01.150 NSOperation[1712:177976] blkop2 == <NSBlockOperation: 0x6080000444d0>
2017-02-12 19:36:01.150 NSOperation[1712:177976] (
"<NSBlockOperation: 0x608000044440>",
"<NSBlockOperation: 0x6080000444d0>"
)
2017-02-12 19:36:01.150 NSOperation[1712:177976] op == <NSBlockOperation: 0x608000044440>
2017-02-12 19:36:01.150 NSOperation[1712:177976] op == <NSBlockOperation: 0x6080000444d0>
2017-02-12 19:36:01.150 NSOperation[1712:178020] 執行blkop1
2017-02-12 19:36:01.151 NSOperation[1712:178021] 執行blkop2
解析:
- 上面創建了兩個blockOperation并且分別設置了優先級。顯然blkop1的優先級低于blkop2的優先級。然后調用了隊列的addOperation:方法使操作入隊。最后輸出結果證明,操作在對列中的順序取決于addOperation:方法而不是優先級。
- 雖然blkop2優先級高于blkop1,但是bloop1卻先于blkop2執行完成。所以,優先級高的操作不一定先執行完成。
- blkop2優先級高于blkop1,則代表blkop2先被執行,但是由于多線程的存在,blkop1可能會與blkop2并行執行。
注意:
(1)優先級只能應用于相同queue中的operations。
(2)操作的優先級高低不等于操作在隊列中排列的順序。換句話說,優先級高的操作不代表一定排在隊列的前面。后入隊的操作有可能因為優先級高而先被執行。PS:操作在隊列中的順序取決于隊列的addOperation:方法。
(3)優先級高只代表先被執行。不代表操作先被執行完成。執行完成的早晚還取決于操作耗時長短。
(4)優先級不能替代依賴,優先級也絕不等于依賴。優先級只是對已經準備好的操作確定其執行順序。
(5)操作的執行優先滿足依賴關系,然后再滿足優先級。即先根據依賴執行操作,然后再從所有準備好的操作中取出優先級最高的那一個執行。
7、:waitUntilAllOperationsAreFinished
為了最佳的性能,你應該設計你的應用盡可能地異步操作,讓應用在Operation正在執行時可以去處理其它事情。如果需要在當前線程中處理operation完成后的結果,可以使用NSOperation的waitUntilFinished方法阻塞當前線程,等待operation完成。通常我們應該避免編寫這樣的代碼,阻塞當前線程可能是一種簡便的解決方案,但是它引入了更多的串行代碼,限制了整個應用的并發性,同時也降低了用戶體驗。絕對不要在應用主線程中等待一個Operation,只能在第二或次要線程中等待。阻塞主線程將導致應用無法響應用戶事件,應用也將表現為無響應。
// 會阻塞當前線程,等到某個operation執行完畢
[operation waitUntilFinished];
除了等待單個Operation完成,你也可以同時等待一個queue中的所有操作,使用NSOperationQueue的waitUntilAllOperationsAreFinished方法。注意:在等待一個 queue時,應用的其它線程仍然可以往queue中添加Operation,因此可能會加長線程的等待時間。
// 阻塞當前線程,等待queue的所有操作執行完畢
[queue waitUntilAllOperationsAreFinished];
注意:waitUntilAllOperationsAreFinished一定要在操作隊列添加了操作后再設置。即,先向operation queue中添加operation,再調用[operationQueue waitUntilAllOperationsAreFinished]。
NSBlockOperation *blkop = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"執行操作 %@",[NSThread currentThread]);
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:blkop];
// waitUntilAllOperationsAreFinished就像GCD的barrier一樣起到隔離作用
// waitUntilAllOperationsAreFinished必須要在操作添加到隊列后設置
// waitUntilAllOperationsAreFinished必須要在NSLog(@"finish");之前設置waitUntilAllOperationsAreFinished
[queue waitUntilAllOperationsAreFinished];
NSLog(@"finish");
8、有了GCD,為什么還有要用NSOperationQueue
GCD是底層的C語言構成的API,而NSOperationQueue以及相關對象是基于GCD的Objective-C對象的封裝,作為一個對象,NSOperationQueue為我們提供了更多的選擇
NSOperationQueue任務可以很方便的取消(也只能取消未執行的任務),而GCD沒法停止已經加入隊列的任務(其實是有的,但需要許多復雜的代碼)
不像GCD那樣的是按FIFO順序來執行的,NSOperation能夠方便地通過依賴關系設置操作執行順序,可以控制任務在特定的任務執行完后才執行;而GCD要實現這個功能的話,就需要通過barrier或者group來控制執行順序,如果依賴關系復雜的話,代碼邏輯就非常復雜了
NSOperation支持KVO(Key-Value Observing),可以方便的監聽任務的狀態(完成、執行中、取消等等狀態)
NSOperation可以設置同一個隊列中任務的優先級,能夠使同一個并行隊列中的任務區分先后地執行,而在GCD中,我們只能區分不同任務隊列的優先級,如果要區分block任務的優先級,也需要大量的復雜代碼
還可以通過自定義NSOperation,封裝任務邏輯,提高整個代碼的復用度
綜合比較其各自使用范圍如下
GCD更接近底層,而NSOperationQueue則更高級抽象,所以GCD在追求性能的底層操作來說,是速度最快的。
從異步操作之間的事務性,順序行,依賴關系。GCD需要自己寫更多的代碼來實現,而NSOperationQueue已經內建了這些支持
如果異步操作的過程需要更多的被交互和UI呈現出來,NSOperationQueue會是一個更好的選擇。底層代碼中,任務之間不太互相依賴,而需要更高的并發能力,GCD則更有優勢
9、小結
雖然在iOS開發中,多線程方法大部分使用的還是GCD,但是對于某些特殊需求,如取消任務、設置任務執行順序、任務狀態監聽、復雜任務封裝等還是推薦使用NSOperationQueue,實現起來會方便很多。
至此,iOS中常用的多線程方式介紹完畢。
相關系列文章
iOS多線程詳解(一)--- 多線程基礎
iOS多線程詳解(二)--- pthread&NSThread
iOS多線程詳解(三)--- GCD
iOS多線程詳解(四)--- NSOperation
iOS多線程詳解(五)--- 線程安全(鎖的創建)
iOS多線程詳解(六)--- 線程安全(Property)
參考文獻
本文主要參考了下面的鏈接,特向作者表示感謝
http://www.lxweimin.com/p/52fe1b85c404
http://www.lxweimin.com/p/4b1d77054b35
http://www.lxweimin.com/p/e6cd25f6da91