我們在前面兩節分別講了iOS多線程的Pthrea、NSThread和GCD,那么我們關于多線程的學習就剩下最后一個內容,就是NSOperation。
NSOperation NSOperation其實是對GCD的封裝,表示了一個獨立的計算單元。NSOperation本身是一個抽象類,對我們來說并沒有什么實用價值,但是系統幫我們封裝了兩個它的子類,并且我們也可以自己去封裝,它的之類都是用線程安全的方式來建立狀態、優先級、依賴性和取消等的模型。
很多執行任務類型的案例都很好的運用了NSOperation,包括網絡請求,圖像壓縮,自然語言處理或者其他很多需要返回處理后數據的、可重復的、結構化的、相對長時間運行的任務。
前面學習GCD的時候我們知道,GCD有兩個核心概念就是任務和隊列,不能只創建任務,然后置之不理,那樣并沒有什么用處,NSOperation也是一樣的,我們不能僅僅把計算單元做好之后就不管它了,我們還是和GCD一樣把它放進一個隊列中進行調度,這樣我們的計算單元才會運作起來,這時候我們就需要另一個概念:NSOperationQueue。
NSOperationQueue 控制著這些操作的執行,它扮演者任務調度的角色,它總是能在遵循先進先出的原則下‘讓高優先級操作’能先于‘低優先級操作’運行,使它管理的操作能基本。
接下來我們先不著急看NSOperation和NSOperationQueue給了我們什么接口,等我們先學習NSOperation有哪些操作可以使用。
狀態
NSOperation包含了一個十分優雅的狀態機來描述每一個操作的執行。
isReady → isExecuting → isFinished
為了替代不那么清晰的state屬性,狀態直接由上面那些keypath的KVO通知決定,也就是說,當一個操作在準備好被執行的時候,它發送了一個KVO通知給isReady的keypath,讓這個keypath對應的屬性isReady在被訪問的時候返回YES。
每一個屬性對于其他的屬性必須是互相獨立不同的,也就是同時只可能有一個屬性返回YES,從而才能維護一個連續的狀態: - isReady: 返回 YES 表示操作已經準備好被執行, 如果返回NO則說明還有其他沒有先前的相關步驟沒有完成。 - isExecuting: 返回YES表示操作正在執行,反之則沒在執行。 - isFinished : 返回YES表示操作執行成功或者被取消了,NSOperationQueue只有當它管理的所有操作的isFinished屬性全標為YES以后操作才停止出列,也就是隊列停止運行,所以正確實現這個方法對于避免死鎖很關鍵。
取消
早些取消那些沒必要的操作是十分有用的。取消的原因可能包括用戶的明確操作或者某個相關的操作失敗。
與之前的執行狀態類似,當NSOperation的-cancel狀態調用的時候會通過KVO通知isCancelled的keypath來修改isCancelled屬性的返回值,NSOperation需要盡快地清理一些內部細節,而后到達一個合適的最終狀態。這個時候isCancelled和isFinished的值將是YES,而isExecuting的值則為NO。
優先級
不可能所有的操作都是一樣重要,通過以下的順序設置queuePriority屬性可以加快或者推遲操作的執行:
NSOperationQueuePriorityVeryHigh
NSOperationQueuePriorityHigh
NSOperationQueuePriorityNormal
NSOperationQueuePriorityLow
NSOperationQueuePriorityVeryLow
此外,有些操作還可以指定threadPriority的值,它的取值范圍可以從0.0到1.0,1.0代表最高的優先級。鑒于queuePriority屬性決定了操作執行的順序,threadPriority則指定了當操作開始執行以后的CPU計算能力的分配,如果你不知道這是什么,好吧,你可能根本沒必要知道這是什么。
依賴性
根據你應用的復雜度不同,將大任務再分成一系列子任務一般都是很有意義的,而你能通過NSOperation
的依賴性實現。
比如說,對于服務器下載并壓縮一張圖片的整個過程,你可能會將這個整個過程分為兩個操作(可能你還會用到這個網絡子過程再去下載另一張圖片,然后用壓縮子過程去壓縮磁盤上的圖片)。顯然圖片需要等到下載完成之后才能被調整尺寸,所以我們定義網絡子操作是壓縮子操作的依賴,通過代碼來說就是:
[resizingOperation addDependency:networkingOperation];//設置resizingOperation依賴networkingOperation
[operationQueue addOperation:networkingOperation];//操作添加進隊列
[operationQueue addOperation:resizingOperation];//操作添加進隊列
除非一個操作的依賴的isFinished返回YES,不然這個操作不會開始。時時牢記將所有的依賴關系添加到操作隊列很重要,不然會像走路遇到一條大溝,就走不過去了喲。
此外,確保不要意外地創建依賴循環,像A依賴B,B又依賴A,這也會導致杯具的死鎖。
completionBlock
有一個在iOS 4和Snow Leopard新加入的十分有用的功能就是completionBlock屬性。
每當一個NSOperation執行完畢,它就會調用它的completionBlock屬性一次,這提供了一個非常好的方式讓你能在視圖控制器(View Controller)里或者模型(Model)里加入自己更多自己的代碼邏輯。比如說,你可以在一個網絡請求操作的completionBlock來處理操作執行完以后從服務器下載下來的數據。
對于現在Objective-C程序員必須掌握的工具中,NSOperation依然是最基本的一個。盡管GCD對于內嵌異步操作十分理想,NSOperation依舊提供更復雜、面向對象的計算模型,它對于涉及到各種類型數據、需要重復處理的任務又是更加理想的。在你的下一個項目里使用它吧,讓它及帶給用戶歡樂,你自己也會很開心的。
之前了解過NSOperation的同學們可能看出來了,沒錯,上面就是翻譯Mattt Thompson的,學過iOS的應該都知道這位大神。
下面我們開始學NSOperation和NSOperationQueue給我們提供的接口,然后針對常用接口的用法示例。
執行操作
//如果你子類化NSOperation類,你要重寫start方法,如果你直接沒有子類化,直接使用start,默認是在主線程執行的。
- (void)start;
//如果你子類化NSOperation類,你可以重寫main方法,并且實現它。如果你這樣做,你就沒有必要調用super。
//你絕不能直接調用mian方法,你應該通過調用start方法啟動你的線程。
//如果直接調用好像是會在主線程執行
- (void)main;
//在NSOperation的任務完成之后要執行的方法塊
@property(copy) void (^completionBlock)(void);
獲取Operation狀態
//Operation是否被取消,getter方法用isCancelled
@property (readonly, getter=isCancelled) BOOL cancelled;
//取消一個Operation
- (void)cancel;
//Operation是否在執行中,getter方法用isExecuting
@property (readonly, getter=isExecuting) BOOL executing;
//Operation操作是否完成,getter方法用isFinished
@property (readonly, getter=isFinished) BOOL finished;
//Operation操作是否是異步的,getter方法用isConcurrent,不建議使用,被下邊的asynchronous方法取代了
@property (readonly, getter=isConcurrent) BOOL concurrent;
//同concurrent
@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);
//Operation操作是否已經準備就緒,getter方法用isFinished
@property (readonly, getter=isReady) BOOL ready;
依賴關系
依賴關系可以有效的安排操作的順序
//添加依賴,就是在你添加的依賴Operation完成之后當前Operation才能開始執行
- (void)addDependency:(NSOperation *)op;
//刪除一個之前添加的依賴Operation
- (void)removeDependency:(NSOperation *)op;
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
優先級
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
}; //優先級
//NSOperationQueue的優先級,對應上面的枚舉
@property NSOperationQueuePriority queuePriority;
//這是線程優先級,已經被棄用了
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
//這個東西在NSThread的時候已經說過了
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
在NSThread的時候講的NSQualityOfService,想看就看一下,在最后。
等待NSOperation完成
//知道operation完成之前,后面的代碼都不會在執行,
//所以為了避免死鎖,在operation啟動之前不要調用該方法
- (void)waitUntilFinished;
NSOperationQueue
//添加一個操作
- (void)addOperation:(NSOperation *)op;
//添加一組Operation并且等到數組中的操作全部完成才會繼續執行
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);
//快速添加一個operation任務
- (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);
//隊列中的operations
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
//操作的數量
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
//最大并發數
@property NSInteger maxConcurrentOperationCount;
//隊列掛起,getter方法為isSuspended
@property (getter=isSuspended) BOOL suspended;
//隊列名字
@property (nullable, copy) NSString *name NS_AVAILABLE(10_6, 4_0);
//說過了,去看NSThread最那里有寫http://www.lxweimin.com/p/b1962d8543ca
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
//正在執行的操作的調度隊列,默認值是nil,只有在隊列中沒有正在執行或排隊的操作才能被設置這個屬性,否則跑出NSInvalidArgumentException異常。
@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);
//取消隊列中的所有操作,正在執行的操作不取消
- (void)cancelAllOperations;
//和Operations的waitUntilFinished方法一樣,這個是等待隊列中所有的操作完成
- (void)waitUntilAllOperationsAreFinished;
//獲取當前的操作隊列,只讀屬性
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue NS_AVAILABLE(10_6, 4_0);
//獲取主隊列,只讀屬性
@property (class, readonly, strong) NSOperationQueue *mainQueue NS_AVAILABLE(10_6, 4_0);
前面說了系統幫我們封裝好了NSOperation的兩個子類,下面我們就用這兩個子類實現一下NSOperation的常用方法。
終于說到了NSOperation的子類,看一下NSInvocationOperation和NSBlockOperation兩個的基本用法。
首先是不配合NSOperationQueue,看一下什么效果
NSLog(@"----");
NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil]; //創建一個NSInvocationOperation 類型的Operation,綁定funcation方法
[invocation start];
NSLog(@"----");
NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1]; //線程睡眠
NSLog(@"---NSBlockOperation--%@", [NSThread currentThread]);
}]; //創建一個NSBlockOperation 類型的Operation,添加塊方法
[block start];
看一下輸出結果
2017-08-25 15:27:25.785 NSOperation[2944:455806] ----
2017-08-25 15:27:26.787 NSOperation[2944:455806] ---invocation---<NSThread: 0x60800007bf40>{number = 1, name = main}
2017-08-25 15:27:26.788 NSOperation[2944:455806] ----
2017-08-25 15:27:27.789 NSOperation[2944:455806] ---NSBlockOperation--<NSThread: 0x60800007bf40>{number = 1, name = main}
從輸出結果看都是在主線程執行中同步執行的,所以不配合NSOperationQueue并沒有什么意義。
NSInvocationOperation
- (void)invocation {
NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
NSInvocationOperation *invocation2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
NSInvocationOperation *invocation3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
//1.創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:invocation];
[queue addOperation:invocation2];
[queue addOperation:invocation3];
}
看一下輸出結果
2017-08-25 15:41:34.088 NSOperation[2989:469234] ---invocation---<NSThread: 0x60000007d440>{number = 4, name = (null)}
2017-08-25 15:41:34.088 NSOperation[2989:469216] ---invocation---<NSThread: 0x608000260800>{number = 5, name = (null)}
2017-08-25 15:41:34.088 NSOperation[2989:469218] ---invocation---<NSThread: 0x6080002607c0>{number = 3, name = (null)}
輸出結果顯示每個Operation都開辟了新線程,并且異步執行,那NSBlockOperation會不會是一樣的呢,來看一下吧
- (void)block {
NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"1----%@",[NSThread currentThread]);
}];
NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"2----%@",[NSThread currentThread]);
}];
NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"3----%@",[NSThread currentThread]);
}];
//創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:block1];
[queue addOperation:block2];
[queue addOperation:block3];
}
一起看一下輸出結果
2017-08-25 15:49:41.484 NSOperation[3013:475949] 1----<NSThread: 0x60000007ea80>{number = 5, name = (null)}
2017-08-25 15:49:41.484 NSOperation[3013:475951] 2----<NSThread: 0x608000070740>{number = 4, name = (null)}
2017-08-25 15:49:41.484 NSOperation[3013:475975] 3----<NSThread: 0x60000007ea00>{number = 3, name = (null)}
和NSInvocationOperation的結果一樣呢,那接下來我們看一下還有什么好用的方法吧。
我們看到NSBlockOperation還有個addExecutionBlock方法,這是干嘛的呢?
- (void)block {
NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"1----%@",[NSThread currentThread]);
}];
NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"2----%@",[NSThread currentThread]);
}];
[block2 addExecutionBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"4---%@", [NSThread currentThread]);
}];
NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"3----%@",[NSThread currentThread]);
}];
//創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:block1];
[queue addOperation:block2];
[queue addOperation:block3];
}
先來看下打印結果再說
2017-08-25 16:10:35.735 NSOperation[3114:492996] 3----<NSThread: 0x608000071180>{number = 5, name = (null)}
2017-08-25 16:10:35.735 NSOperation[3114:493009] 2----<NSThread: 0x6000000711c0>{number = 4, name = (null)}
2017-08-25 16:10:35.735 NSOperation[3114:493008] 1----<NSThread: 0x60800006a500>{number = 3, name = (null)}
2017-08-25 16:10:35.735 NSOperation[3114:492993] 4---<NSThread: 0x600000071280>{number = 6, name = (null)}
從結果來看似乎和新建一個NSBlockOperation效果一樣啊,都是異步執行。
看看隊列有中有什么好用的方法吧
//創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue setMaxConcurrentOperationCount:2];
[queue addOperation:block1];
[queue addOperation:block2];
[queue addOperation:block3];
輸出結果:
2017-08-25 15:53:02.280 NSOperation[3034:479336] 1----<NSThread: 0x60800007b7c0>{number = 3, name = (null)}
2017-08-25 15:53:02.280 NSOperation[3034:479338] 2----<NSThread: 0x600000263d00>{number = 4, name = (null)}
2017-08-25 15:53:03.353 NSOperation[3034:479335] 3----<NSThread: 0x60800007b740>{number = 5, name = (null)}
我們在剛剛的NSBlockOperation測試的基礎上加上了一句[queue setMaxConcurrentOperationCount:2],結果變得不一樣了呢,3要比1和2晚一秒鐘哦,剛好是3的睡眠時間,那就說明3是在1和2完成之后開始的,這就是最大并發量的作用,設置的數字就是它允許開辟的最大操作數數。
最后,我們發現除了剛開始在線程中任務順序執行外,我們一直沒講串行,因為NSOperationQueue是并行隊列,我們想要串行就把最大并發量設置為1就可以了,是不是簡單多了,不需要再怕不小心把串行并行寫錯了。
前面有句話看清楚哦,是最大任務數,不是線程數,看看下面這種情況你就清楚其中的區別了
//創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue setMaxConcurrentOperationCount:1];//設置最大并發量
// [block1 addDependency:block3];//設置block1依賴block3
block1.queuePriority = NSOperationQueuePriorityHigh;
[queue addOperation:block1];
[queue addOperation:block2];
[queue addOperation:block3];
輸出結果:
2017-08-25 16:44:21.361 NSOperation[3366:527290] 1----<NSThread: 0x6000002644c0>{number = 3, name = (null)}
2017-08-25 16:44:22.435 NSOperation[3366:527290] 4---<NSThread: 0x6000002644c0>{number = 3, name = (null)}
2017-08-25 16:44:22.435 NSOperation[3366:527274] 2----<NSThread: 0x600000267c40>{number = 4, name = (null)}
2017-08-25 16:44:23.482 NSOperation[3366:527274] 3----<NSThread: 0x600000267c40>{number = 4, name = (null)}
所以看時間我們能看到2和4是同一時間執行的,所以setMaxConcurrentOperationCount只能控制人物數量。
前面說了好多遍依賴關系,看一下什么是依賴關系吧
//創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue setMaxConcurrentOperationCount:2];//設置最大并發量
[block1 addDependency:block3];//設置block1依賴block3
[queue addOperation:block1];
[queue addOperation:block2];
[queue addOperation:block3];
輸出結果:
2017-08-25 16:00:12.378 NSOperation[3058:484669] 2----<NSThread: 0x608000267d40>{number = 3, name = (null)}
2017-08-25 16:00:12.378 NSOperation[3058:484671] 3----<NSThread: 0x608000267d80>{number = 4, name = (null)}
2017-08-25 16:00:13.380 NSOperation[3058:484668] 1----<NSThread: 0x60000007ae80>{number = 5, name = (null)}
誒,隊列不是先進先出的嗎?為什么線程不夠的時候不是1和2先執行呢?反而3先執行了。噢,我們設置了1依賴3,所以當cpu要殺1的時候,1說“不行,要殺我,先殺3”,所以先執行了2和3,最后執行1。
也可以不初始化NSBlockOperation,直接在隊列中添加block操作哦。
//創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
// [queue setMaxConcurrentOperationCount:1];//設置最大并發量
// [block1 addDependency:block3];//設置block1依賴block3
block1.queuePriority = NSOperationQueuePriorityHigh;
[queue addOperation:block1];
[queue addOperation:block2];
[queue addOperation:block3];
[queue addOperationWithBlock:^{
sleep(1);
NSLog(@"5----%@",[NSThread currentThread]);
}];
輸出結果:
2017-08-25 16:41:30.151 NSOperation[3346:524342] 5----<NSThread: 0x60000007c980>{number = 3, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524343] 1----<NSThread: 0x60000007cb80>{number = 4, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524345] 2----<NSThread: 0x608000071340>{number = 5, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524360] 3----<NSThread: 0x600000075c00>{number = 6, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524359] 4---<NSThread: 0x60800007c780>{number = 7, name = (null)}
看結果和用NSBlockOperation沒什么區別哦
最后的最后,關于iOS多線程就到這里了,希望對后學者能有一些幫助,那將是我的榮幸。