天天看點

iOS多線程程式設計詳情(GCD、NSThread、NSOperation)

GCD

一、簡介

在iOS所有實作多線程的方案中,GCD應該是最有魅力的,因為GCD本身是蘋果公司為多核的并行運算提出的解決方案。GCD在工作時會自動利用更多的處理器核心,以充分利用更強大的機器。GCD是Grand Central Dispatch的簡稱,它是基于C語言的。如果使用GCD,完全由系統管理線程,我們不需要編寫線程代碼。隻需定義想要執行的任務,然後添加到适當的排程隊列(dispatch queue)。GCD會負責建立線程和排程你的任務,系統直接提供線程管理

二、排程隊列(dispath queue)

1.GCD的一個重要概念是隊列,它的核心理念:将長期運作的任務拆分成多個工作單元,并将這些單元添加到dispath queue中,系統會為我們管理這些dispath queue,為我們在多個線程上執行工作單元,我們不需要直接啟動和管理背景線程。

2.系統提供了許多預定義的dispath queue,包括可以保證始終在主線程上執行工作的dispath queue。也可以建立自己的dispath queue,而且可以建立任意多個。GCD的dispath queue嚴格遵循FIFO(先進先出)原則,添加到dispath queue的工作單元将始終按照加入dispath queue的順序啟動。

3.dispatch queue按先進先出的順序,串行或并發地執行任務

1> serial dispatch queue一次隻能執行一個任務, 目前任務完成才開始出列并啟動下一個任務

2> concurrent dispatch queue則盡可能多地啟動任務并發執行

三、建立和管理dispatch queue

1.獲得全局并發Dispatch Queue (concurrent dispatch queue)

1> 并發dispatch queue可以同時并行地執行多個任務,不過并發queue仍然按先進先出的順序來啟動任務。并發queue會在之前的任務完成之前就出列下一個任務并開始執行。并發queue同時執行的任務數量會根據應用和系統動态變化,各種因素包括:可用核數量、其它程序正在執行的工作數量、其它串行dispatch queue中優先任務的數量等.

2> 系統給每個應用提供三個并發dispatch queue,整個應用内全局共享,三個queue的差別是優先級。你不需要顯式地建立這些queue,使用dispatch_get_global_queue函數來擷取這三個queue:

[java]  view plain copy

  1. // 擷取預設優先級的全局并發dispatch queue  
  2. dispatch_queue_t  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  

第一個參數用于指定優先級,分 别使用DISPATCH_QUEUE_PRIORITY_HIGH和DISPATCH_QUEUE_PRIORITY_LOW兩個常 量 來擷取高和低優先級的兩個queue;第二個參數目前未使用到,預設0即可

3> 雖然dispatch queue是引用計數的對象,但你不需要retain和release全局并發queue。因為這些queue對應用是全局的,retain和release調用會被忽略。你也不需要存儲這三個queue的引用,每次都直接調用dispatch_get_global_queue獲得queue就行了。

2.建立串行Dispatch Queue (serial dispatch queue)

1> 應用的任務需要按特定順序執行時,就需要使用串行Dispatch Queue,串行queue每次隻能執行一個任務。你可以使用串行queue來替代鎖,保護共享資源 或可變的資料結構。和鎖不一樣的是,串行queue確定任務按可預測的順序執行。而且隻要你異步地送出任務到串行queue,就永遠不會産生死鎖

2> 你必須顯式地建立和管理所有你使用的串行queue,應用可以建立任意數量的串行queue,但不要為了同時執行更多任務而建立更多的串行queue。如果你需要并發地執行大量任務,應該把任務送出到全局并發queue

3> 利用dispatch_queue_create函數建立串行queue,兩個參數分别是queue名和一組queue屬性

[java]  view plain copy

  1. dispatch_queue_t queue;  
  2. queue = dispatch_queue_create("cn.itcast.queue", NULL);  

3.運作時獲得公共Queue

GCD提供了函數讓應用通路幾個公共dispatch queue:

1> 使用dispatch_get_current_queue函數作為調試用途,或者測試目前queue的辨別。在block對象中調用這個函數會傳回block送出到的queue(這個時候queue應該正在執行中)。在block對象之外調用這個函數會傳回應用的預設并發queue。

2> 使用dispatch_get_main_queue函數獲得應用主線程關聯的串行dispatch queue

3> 使用dispatch_get_global_queue來獲得共享的并發queue

4.Dispatch Queue的記憶體管理

1> Dispatch Queue和其它dispatch對象(還有dispatch source)都是引用計數的資料類型。當你建立一個串行dispatch queue時,初始引用計數為 1,你可以使用dispatch_retain和dispatch_release函數來增加和減少引用計數。當引用計數到達 0 時,系統會異步地銷毀這個queue

2> 對dispatch對象(如dispatch queue)retain和release 是很重要的,確定它們被使用時能夠保留在記憶體中。和OC對象一樣,通用的規則是如果使用一個傳遞過來的queue,你應該在使用前retain,使用完之後release

3> 你不需要retain或release全局dispatch queue,包括全局并發dispatch queue和main dispatch queue

4> 即使你實作的是自動垃圾收集的應用,也需要retain和release建立的dispatch queue和其它dispatch對象。GCD 不支援垃圾收集模型來回收記憶體

四、添加任務到queue

要執行一個任務,你需要将它添加到一個适當的dispatch queue,你可以單個或按組來添加,也可以同步或異步地執行一個任務,也。一旦進入到queue,queue會負責盡快地執行你的任務。一般可以用一個block來封裝任務内容。

1.添加單個任務到queue

1> 異步添加任務

你可以異步或同步地添加一個任務到Queue,盡可能地使用dispatch_async或dispatch_async_f函數異步地排程任務。因為添加任務到Queue中時,無法确定這些代碼什麼時候能夠執行。是以異步地添加block或函數,可以讓你立即排程這些代碼的執行,然後調用線程可以繼續去做其它事情。特别是應用主線程一定要異步地 dispatch 任務,這樣才能及時地響應使用者事件

2> 同步添加任務

少數時候你可能希望同步地排程任務,以避免競争條件或其它同步錯誤。 使用dispatch_sync和dispatch_sync_f函數同步地添加任務到Queue,這兩個函數會阻塞目前調用線程,直到相應任務完成執行。注意:絕對不要在任務中調用 dispatch_sync或dispatch_sync_f函數,并同步排程新任務到目前正在執行的 queue。對于串行queue這一點特别重要,因為這樣做肯定會導緻死鎖;而并發queue也應該避免這樣做。

3> 代碼示範

[java]  view plain copy

  1. // 調用前,檢視下目前線程  
  2. NSLog(@"目前調用線程:%@", [NSThread currentThread]);  
  3. // 建立一個串行queue  
  4. dispatch_queue_t queue = dispatch_queue_create("cn.itcast.queue", NULL);  
  5. dispatch_async(queue, ^{  
  6.     NSLog(@"開啟了一個異步任務,目前線程:%@", [NSThread currentThread]);  
  7. });  
  8. dispatch_sync(queue, ^{  
  9.     NSLog(@"開啟了一個同步任務,目前線程:%@", [NSThread currentThread]);  
  10. });  
  11. // 銷毀隊列  
  12. dispatch_release(queue);  

列印資訊:

[java]  view plain copy

  1. 2013-02-03 09:03:37.348 thread[6491:c07] 目前調用線程:<NSThread: 0x714fa80>{name = (null), num = 1}  
  2. 2013-02-03 09:03:37.349 thread[6491:1e03] 開啟了一個異步任務,目前線程:<NSThread: 0x74520a0>{name = (null), num = 3}  
  3. 2013-02-03 09:03:37.350 thread[6491:c07] 開啟了一個同步任務,目前線程:<NSThread: 0x714fa80>{name = (null), num = 1}  

2.并發地執行循環疊代

如果你使用循環執行固定次數的疊代, 并發dispatch queue可能會提高性能。

例如下面的for循環:

[java]  view plain copy

  1. int i;  
  2. int count = 10;  
  3. for (i = 0; i < count; i++) {  
  4.    printf("%d  ",i);  
  5. }  

1> 如果每次疊代執行的任務與其它疊代獨立無關,而且循環疊代執行順序也無關緊要的話,你可以調用dispatch_apply或dispatch_apply_f函數來替換循環。這兩個函數為每次循環疊代将指定的block或函數送出到queue。當dispatch到并發 queue時,就有可能同時執行多個循環疊代。用dispatch_apply或dispatch_apply_f時你可以指定串行或并發 queue。并發queue允許同時執行多個循環疊代,而串行queue就沒太大必要使用了。

下面代碼使用dispatch_apply替換了for循環,你傳遞的block必須包含一個size_t類型的參數,用來辨別目前循環疊代。第一次疊代這個參數值為0,最後一次值為count - 1

[java]  view plain copy

  1. // 獲得全局并發queue  
  2. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
  3. size_t count = 10;  
  4. dispatch_apply(count, queue, ^(size_t i) {  
  5.     printf("%zd ", i);  
  6. });  
  7. // 銷毀隊列  
  8. dispatch_release(queue);  

列印資訊:

[java]  view plain copy

  1. 1 2 0 3 4 5 6 7 8 9   

可以看出,這些疊代是并發執行的

和普通for循環一樣,dispatch_apply和dispatch_apply_f函數也是在所有疊代完成之後才會傳回,是以這兩個函數會阻塞目前線程,主線程中調用這兩個函數必須小心,可能會阻止事件處理循環并無法響應使用者事件。是以如果循環代碼需要一定的時間執行,可以考慮在另一個線程中調用這兩個函數。如果你傳遞的參數是串行queue,而且正是執行目前代碼的queue,就會産生死鎖。

3.在主線程中執行任務

1> GCD提供一個特殊的dispatch queue,可以在應用的主線程中執行任務。隻要應用主線程設定了run loop(由CFRunLoopRef類型或NSRunLoop對象管理),就會自動建立這個queue,并且最後會自動銷毀。非Cocoa應用如果不顯式地設定run loop, 就必須顯式地調用dispatch_main函數來顯式地激活這個dispatch queue,否則雖然你可以添加任務到queue,但任務永遠不會被執行。

2> 調用dispatch_get_main_queue函數獲得應用主線程的dispatch queue,添加到這個queue的任務由主線程串行化執行

3> 代碼實作,比如異步下載下傳圖檔後,回到主線程顯示圖檔

[java]  view plain copy

  1. // 異步下載下傳圖檔  
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
  3.     NSURL *url = [NSURL URLWithString:@"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg"];  
  4.     UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];  
  5.     // 回到主線程顯示圖檔  
  6.     dispatch_async(dispatch_get_main_queue(), ^{  
  7.         self.imageView.image = image;  
  8.     });  
  9. });  

4.任務中使用Objective-C對象

GCD支援Cocoa記憶體管理機制,是以可以在送出到queue的block中自由地使用Objective-C對象。每個dispatch queue維護自己的autorelease pool確定釋放autorelease對象,但是queue不保證這些對象實際釋放的時間。如果應用消耗大量記憶體,并且建立大量autorelease對象,你需要建立自己的autorelease pool,用來及時地釋放不再使用的對象。

五、暫停和繼續queue

我們可以使用dispatch_suspend函數暫停一個queue以阻止它執行block對象;使用dispatch_resume函數繼續dispatch queue。調用dispatch_suspend會增加queue的引用計數,調用dispatch_resume則減少queue的引用計數。當引用計數大于0時,queue就保持挂起狀态。是以你必須對應地調用suspend和resume函數。挂起和繼續是異步的,而且隻在執行block之間(比如在執行一個新的block之前或之後)生效。挂起一個queue不會導緻正在執行的block停止。

六、Dispatch Group的使用

假設有這樣一個需求:從網絡上下載下傳兩張不同的圖檔,然後顯示到不同的UIImageView上去,一般可以這樣實作

[java]  view plain copy

  1. // 根據url擷取UIImage  
  2. - (UIImage *)imageWithURLString:(NSString *)urlString {  
  3.     NSURL *url = [NSURL URLWithString:urlString];  
  4.     NSData *data = [NSData dataWithContentsOfURL:url];  
  5.     return [UIImage imageWithData:data];  
  6. }  
  7. - (void)downloadImages {  
  8.     // 異步下載下傳圖檔  
  9.     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
  10.         // 下載下傳第一張圖檔  
  11.         NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
  12.         UIImage *image1 = [self imageWithURLString:url1];  
  13.         // 下載下傳第二張圖檔  
  14.         NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
  15.         UIImage *image2 = [self imageWithURLString:url2];  
  16.         // 回到主線程顯示圖檔  
  17.         dispatch_async(dispatch_get_main_queue(), ^{  
  18.             self.imageView1.image = image1;  
  19.             self.imageView2.image = image2;  
  20.         });  
  21.     });  
  22. }  

雖然這種方 案可以解決問題,但其實兩張圖檔的下載下傳過程并不需要按順序執行,并發執行它們可以提高執行速度。有個注意點就是必須等兩張圖檔都下載下傳完畢後才能回到主線程顯示圖檔。Dispatch Group能夠在這種情況下幫我們提升性能。下面先看看Dispatch Group的用處:

我們可以使用dispatch_group_async函數将多個任務關聯到一個Dispatch Group和相應的queue中,group會并發地同時執行這些任務。而且Dispatch Group可以用來阻塞一個線程, 直到group關聯的所有的任務完成執行。有時候你必須等待任務完成的結果,然後才能繼續後面的處理。

下面用Dispatch Group優化上面的代碼:

[java]  view plain copy

  1. // 根據url擷取UIImage  
  2. - (UIImage *)imageWithURLString:(NSString *)urlString {  
  3.     NSURL *url = [NSURL URLWithString:urlString];  
  4.     NSData *data = [NSData dataWithContentsOfURL:url];  
  5.     // 這裡并沒有自動釋放UIImage對象  
  6.     return [[UIImage alloc] initWithData:data];  
  7. }  
  8. - (void)downloadImages {  
  9.     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
  10.     // 異步下載下傳圖檔  
  11.     dispatch_async(queue, ^{  
  12.         // 建立一個組  
  13.         dispatch_group_t group = dispatch_group_create();  
  14.         __block UIImage *image1 = nil;  
  15.         __block UIImage *image2 = nil;  
  16.         // 關聯一個任務到group  
  17.         dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
  18.             // 下載下傳第一張圖檔  
  19.             NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
  20.             image1 = [self imageWithURLString:url1];  
  21.         });  
  22.         // 關聯一個任務到group  
  23.         dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
  24.             // 下載下傳第一張圖檔  
  25.             NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
  26.             image2 = [self imageWithURLString:url2];  
  27.         });  
  28.         // 等待組中的任務執行完畢,回到主線程執行block回調  
  29.         dispatch_group_notify(group, dispatch_get_main_queue(), ^{  
  30.             self.imageView1.image = image1;  
  31.             self.imageView2.image = image2;  
  32.             // 千萬不要在異步線程中自動釋放UIImage,因為當異步線程結束,異步線程的自動釋放池也會被銷毀,那麼UIImage也會被銷毀  
  33.             // 在這裡釋放圖檔資源  
  34.             [image1 release];  
  35.             [image2 release];  
  36.         });  
  37.         // 釋放group  
  38.         dispatch_release(group);  
  39.     });  
  40. }  

dispatch_group_notify函數用來指定一個額外的block,該block将在group中所有任務完成後執行

NSThread

介紹:每個iOS應用程式都有個專門用來更新顯示UI界面、處理使用者的觸摸事件的主線程,是以不能将其他太耗時的操作放在主線程中執行,不然會造成主線程堵塞(出現卡機現象),帶來極壞的使用者體驗。一般的解決方案就是将那些耗時的操作放到另外一個線程中去執行,多線程程式設計是防止主線程堵塞,增加運作效率的最佳方法

iOS支援多個層次的多線程程式設計,層次越高的抽象程度越高,使用也越友善,也是蘋果最推薦使用的方法。下面根據抽象層次從低到高依次列出iOS所支援的多線程程式設計方法:

1.Thread :是三種方法裡面相對輕量級的,但需要管理線程的生命周期、同步、加鎖問題,這會導緻一定的性能開銷

2.Cocoa Operations:是基于OC實作的,NSOperation以面向對象的方式封裝了需要執行的操作,不必關心線程管理、同步等問題。NSOperation是一個抽象基類,iOS提供了兩種預設實作:NSInvocationOperation和NSBlockOperation,當然也可以自定義NSOperation

3.Grand Central Dispatch(簡稱GCD,iOS4才開始支援):提供了一些新特性、運作庫來支援多核并行程式設計,它的關注點更高:如何在多個cpu上提升效率

這篇文章簡單介紹了第一種多線程程式設計的方式,主要是利用NSThread這個類,一個NSThread執行個體代表着一條線程

一、NSthread的初始化

1.動态方法

[java]  view plain copy

  1. - (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument;  

[java]  view plain copy

  1. // 初始化線程  
  2. NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];  
  3. // 設定線程的優先級(0.0 - 1.0,1.0最進階)  
  4. thread.threadPriority = 1;  
  5. // 開啟線程  
  6. [thread start];  

參數解析:

selector :線程執行的方法,這個selector最多隻能接收一個參數

target :selector消息發送的對象

argument : 傳給selector的唯一參數,也可以是nil

2.靜态方法

[java]  view plain copy

  1. + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;  

[java]  view plain copy

  1. [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];  
  2. // 調用完畢後,會馬上建立并開啟新線程  

3.隐式建立線程的方法

[java]  view plain copy

  1. [self performSelectorInBackground:@selector(run) withObject:nil];  

二、擷取目前線程

[java]  view plain copy

  1. NSThread *current = [NSThread currentThread];  

三、擷取主線程

[java]  view plain copy

  1. NSThread *main = [NSThread mainThread];  

四、暫停目前線程

[java]  view plain copy

  1. // 暫停2s  
  2. [NSThread sleepForTimeInterval:2];  
  3. // 或者  
  4. NSDate *date = [NSDate dateWithTimeInterval:2 sinceDate:[NSDate date]];  
  5. [NSThread sleepUntilDate:date];  

五、線程間的通信

1.在指定線程上執行操作

[java]  view plain copy

  1. [self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES];  

2.在主線程上執行操作

[java]  view plain copy

  1. [self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES];  

3.在目前線程執行操作

[java]  view plain copy

  1. [self performSelector:@selector(run) withObject:nil];  

六、優缺點

1.優點:NSThread比其他兩種多線程方案較輕量級,更直覺地控制線程對象

2.缺點:需要自己管理線程的生命周期,線程同步。線程同步對資料的加鎖會有一定的系統開銷

NSOperation

一、NSOperation

1.簡介

NSOperation執行個體封裝了需要執行的操作和執行操作所需的資料,并且能夠以并發或非并發的方式執行這個操作。

NSOperation本身是抽象基類,是以必須使用它的子類,使用NSOperation子類的方式有2種:

1> Foundation架構提供了兩個具體子類直接供我們使用:NSInvocationOperation和NSBlockOperation

2> 自定義子類繼承NSOperation,實作内部相應的方法

2.執行操作

NSOperation調用start方法即可開始執行操作,NSOperation對象預設按同步方式執行,也就是在調用start方法的那個線程中直接執行。NSOperation對象的isConcurrent方法會告訴我們這個操作相對于調用start方法的線程,是同步還是異步執行。isConcurrent方法預設傳回NO,表示操作與調用線程同步執行

3.取消操作

operation開始執行之後, 預設會一直執行操作直到完成,我們也可以調用cancel方法中途取消操作

[java]  view plain copy

  1. [operation cancel];  

4.監聽操作的執行

如果我們想在一個NSOperation執行完畢後做一些事情,就調用NSOperation的setCompletionBlock方法來設定想做的事情

[java]  view plain copy

  1. operation.completionBlock = ^() {  
  2.     NSLog(@"執行完畢");  
  3. };  

或者

[java]  view plain copy

  1. [operation setCompletionBlock:^() {  
  2.     NSLog(@"執行完畢");  
  3. }];  

二、NSInvocationOperation

1.簡介

基于一個對象和selector來建立操作。如果你已經有現有的方法來執行需要的任務,就可以使用這個類

2.建立并執行操作

[java]  view plain copy

  1. // 這個操作是:調用self的run方法  
  2. NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];  
  3. // 開始執行任務(同步執行)  
  4. [operation start];  

三、NSBlockOperation

1.簡介

能夠并發地執行一個或多個block對象,所有相關的block都執行完之後,操作才算完成

2.建立并執行操作

[java]  view plain copy

  1. NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){  
  2.         NSLog(@"執行了一個新的操作,線程:%@", [NSThread currentThread]);  
  3. }];  
  4.  // 開始執行任務(這裡還是同步執行)  
  5. [operation start];  

3.通過addExecutionBlock方法添加block操作

[java]  view plain copy

  1. NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){  
  2.     NSLog(@"執行第1次操作,線程:%@", [NSThread currentThread]);  
  3. }];  
  4. [operation addExecutionBlock:^() {  
  5.     NSLog(@"又執行了1個新的操作,線程:%@", [NSThread currentThread]);  
  6. }];  
  7. [operation addExecutionBlock:^() {  
  8.     NSLog(@"又執行了1個新的操作,線程:%@", [NSThread currentThread]);  
  9. }];  
  10. [operation addExecutionBlock:^() {  
  11.     NSLog(@"又執行了1個新的操作,線程:%@", [NSThread currentThread]);  
  12. }];  
  13. // 開始執行任務  
  14. [operation start];  

列印資訊如下:

[java]  view plain copy

  1. 2013-02-02 21:38:46.102 thread[4602:c07] 又執行了1個新的操作,線程:<NSThread: 0x7121d50>{name = (null), num = 1}  
  2. 2013-02-02 21:38:46.102 thread[4602:3f03] 又執行了1個新的操作,線程:<NSThread: 0x742e1d0>{name = (null), num = 5}  
  3. 2013-02-02 21:38:46.102 thread[4602:1b03] 執行第1次操作,線程:<NSThread: 0x742de50>{name = (null), num = 3}  
  4. 2013-02-02 21:38:46.102 thread[4602:1303] 又執行了1個新的操作,線程:<NSThread: 0x7157bf0>{name = (null), num = 4}  

可以看出,這4個block是并發執行的,也就是在不同線程中執行的,num屬性可以看成是線程的id

四、自定義NSOperation

1.簡介

如果NSInvocationOperation和NSBlockOperation對象不能滿足需求, 你可以直接繼承NSOperation, 并添加任何你想要的行為。繼承所需的工作量主要取決于你要實作非并發還是并發的NSOperation。定義非并發的NSOperation要簡單許多,隻需要重載-(void)main這個方法,在這個方法裡面執行主任務,并正确地響應取消事件; 對于并發NSOperation, 你必須重寫NSOperation的多個基本方法進行實作(這裡暫時先介紹非并發的NSOperation)

2.非并發的NSOperation

比如叫做DownloadOperation,用來下載下傳圖檔

1> 繼承NSOperation,重寫main方法,執行主任務

DownloadOperation.h

[java]  view plain copy

  1. #import <Foundation/Foundation.h>  
  2. @protocol DownloadOperationDelegate;  
  3. @interface DownloadOperation : NSOperation  
  4. // 圖檔的url路徑  
  5. @property (nonatomic, copy) NSString *imageUrl;  
  6. // 代理  
  7. @property (nonatomic, retain) id<DownloadOperationDelegate> delegate;  
  8. - (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate;  
  9. @end  
  10. // 圖檔下載下傳的協定  
  11. @protocol DownloadOperationDelegate <NSObject>  
  12. - (void)downloadFinishWithImage:(UIImage *)image;  
  13. @end  

DownloadOperation.m

[java]  view plain copy

  1. #import "DownloadOperation.h"  
  2. @implementation DownloadOperation  
  3. @synthesize delegate = _delegate;  
  4. @synthesize imageUrl = _imageUrl;  
  5. // 初始化  
  6. - (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate {  
  7.     if (self = [super init]) {  
  8.         self.imageUrl = url;  
  9.         self.delegate = delegate;  
  10.     }  
  11.     return self;  
  12. }  
  13. // 釋放記憶體  
  14. - (void)dealloc {  
  15.     [super dealloc];  
  16.     [_delegate release];  
  17.     [_imageUrl release];  
  18. }  
  19. // 執行主任務  
  20. - (void)main {  
  21.     // 建立一個自動釋放池,如果是異步執行操作,那麼将無法通路到主線程的自動釋放池  
  22.     @autoreleasepool {  
  23.         // ....  
  24.     }  
  25. }  
  26. @end  

2> 正确響應取消事件

operation開始執行之後,會一直執行任務直到完成,或者顯式地取消操作。取消可能發生在任何時候,甚至在operation執行之前。盡管NSOperation提供了一個方法,讓應用取消一個操作,但是識别出取消事件則是我們自己的事情。如果operation直接終止, 可能無法回收所有已配置設定的記憶體或資源。是以operation對象需要檢測取消事件,并優雅地退出執行

NSOperation對象需要定期地調用isCancelled方法檢測操作是否已經被取消,如果傳回YES(表示已取消),則立即退出執行。不管是自定義NSOperation子類,還是使用系統提供的兩個具體子類,都需要支援取消。isCancelled方法本身非常輕量,可以頻繁地調用而不産生大的性能損失

以下地方可能需要調用isCancelled:

* 在執行任何實際的工作之前

* 在循環的每次疊代過程中,如果每個疊代相對較長可能需要調用多次

* 代碼中相對比較容易中止操作的任何地方

DownloadOperation的main方法實作如下

[java]  view plain copy

  1. - (void)main {  
  2.     // 建立一個自動釋放池,如果是異步執行操作,那麼将無法通路到主線程的自動釋放池  
  3.     @autoreleasepool {  
  4.         if (self.isCancelled) return;  
  5.         // 擷取圖檔資料  
  6.         NSURL *url = [NSURL URLWithString:self.imageUrl];  
  7.         NSData *imageData = [NSData dataWithContentsOfURL:url];  
  8.         if (self.isCancelled) {  
  9.             url = nil;  
  10.             imageData = nil;  
  11.             return;  
  12.         }  
  13.         // 初始化圖檔  
  14.         UIImage *image = [UIImage imageWithData:imageData];  
  15.         if (self.isCancelled) {  
  16.             image = nil;  
  17.             return;  
  18.         }  
  19.         if ([self.delegate respondsToSelector:@selector(downloadFinishWithImage:)]) {  
  20.             // 把圖檔資料傳回到主線程  
  21.             [(NSObject *)self.delegate performSelectorOnMainThread:@selector(downloadFinishWithImage:) withObject:image waitUntilDone:NO];  
  22.         }  
  23.     }  
  24. }  

NSOperationQueue

一、簡介

一個NSOperation對象可以通過調用start方法來執行任務,預設是同步執行的。也可以将NSOperation添加到一個NSOperationQueue(操作隊列)中去執行,而且是異步執行的。

建立一個操作隊列:

[java]  view plain copy

  1. NSOperationQueue *queue = [[NSOperationQueue alloc] init];  

二、添加NSOperation到NSOperationQueue中

1.添加一個operation

[java]  view plain copy

  1. [queue addOperation:operation];  

2.添加一組operation

[java]  view plain copy

  1. [queue addOperations:operations waitUntilFinished:NO];  

3.添加一個block形式的operation

[java]  view plain copy

  1. [queue addOperationWithBlock:^() {  
  2.     NSLog(@"執行一個新的操作,線程:%@", [NSThread currentThread]);  
  3. }];  

NSOperation添加到queue之後,通常短時間内就會得到運作。但是如果存在依賴,或者整個queue被暫停等原因,也可能需要等待。

注意:NSOperation添加到queue之後,絕對不要再修改NSOperation對象的狀态。因為NSOperation對象可能會在任何時候運作,是以改變NSOperation對象的依賴或資料會産生不利的影響。你隻能檢視NSOperation對象的狀态, 比如是否正在運作、等待運作、已經完成等

三、添加NSOperation的依賴對象

1.當某個NSOperation對象依賴于其它NSOperation對象的完成時,就可以通過addDependency方法添加一個或者多個依賴的對象,隻有所有依賴的對象都已經完成操作,目前NSOperation對象才會開始執行操作。另外,通過removeDependency方法來删除依賴對象。

[java]  view plain copy

  1. [operation2 addDependency:operation1];  

依賴關系不局限于相同queue中的NSOperation對象,NSOperation對象會管理自己的依賴, 是以完全可以在不同的queue之間的NSOperation對象建立依賴關系

iOS多線程程式設計詳情(GCD、NSThread、NSOperation)

唯一的限制是不能建立環形依賴,比如A依賴B,B依賴A,這是錯誤的

2.依賴關系會影響到NSOperation對象在queue中的執行順序,看下面的例子:

1> 沒有設定依賴關系

[java]  view plain copy

  1. NSOperationQueue *queue = [[NSOperationQueue alloc] init];  
  2. NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^(){  
  3.     NSLog(@"執行第1次操作,線程:%@", [NSThread currentThread]);  
  4. }];  
  5. NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^(){  
  6.     NSLog(@"執行第2次操作,線程:%@", [NSThread currentThread]);  
  7. }];  
  8. [queue addOperation:operation1];  
  9. [queue addOperation:operation2];  

列印資訊:

[java]  view plain copy

  1. 2013-02-03 00:21:35.024 thread[5616:3d13] 執行第1次操作,線程:<NSThread: 0x7658570>{name = (null), num = 3}  
  2. 2013-02-03 00:21:35.063 thread[5616:1303] 執行第2次操作,線程:<NSThread: 0x765a2e0>{name = (null), num = 4}  

可以看出,預設是按照添加順序執行的,先執行operation1,再執行operation2

2> 設定了依賴關系

[java]  view plain copy

  1. NSOperationQueue *queue = [[NSOperationQueue alloc] init];  
  2. NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^(){  
  3.     NSLog(@"執行第1次操作,線程:%@", [NSThread currentThread]);  
  4. }];  
  5. NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^(){  
  6.     NSLog(@"執行第2次操作,線程:%@", [NSThread currentThread]);  
  7. }];  
  8. // operation1依賴于operation2  
  9. [operation1 addDependency:operation2];  
  10. [queue addOperation:operation1];  
  11. [queue addOperation:operation2];  

列印資訊:

[java]  view plain copy

  1. 2013-02-03 00:24:16.260 thread[5656:1b03] 執行第2次操作,線程:<NSThread: 0x7634490>{name = (null), num = 3}  
  2. 2013-02-03 00:24:16.285 thread[5656:1303] 執行第1次操作,線程:<NSThread: 0x9138b50>{name = (null), num = 4}  

可以看出,先執行operation2,再執行operation1

四、修改Operations的執行順序

對于添加到queue中的operations,它們的執行順序取決于2點:

1.首先看看NSOperation是否已經準備好:是否準備好由對象的依賴關系确定

2.然後再根據所有NSOperation的相對優先級來确定。優先級等級則是operation對象本身的一個屬性。預設所有operation都擁有“普通”優先級,不過可以通過setQueuePriority:方法來提升或降低operation對象的優先級。優先級隻能應用于相同queue中的operations。如果應用有多個operation queue,每個queue的優先級等級是互相獨立的。是以不同queue中的低優先級操作仍然可能比高優先級操作更早執行。

注意:優先級不能替代依賴關系,優先級隻是對已經準備好的 operations确定執行順序。先滿足依賴關系,然後再根據優先級從所有準備好的操作中選擇優先級最高的那個執行。

五、設定隊列的最大并發操作數量

隊列的最大并發操作數量,意思是隊列中最多同時運作幾條線程

雖然NSOperationQueue類設計用于并發執行Operations,你也可以強制單個queue一次隻能執行一個Operation。setMaxConcurrentOperationCount:方法可以配置queue的最大并發操作數量。設為1就表示queue每次隻能執行一個操作。不過operation執行的順序仍然依賴于其它因素,比如operation是否準備好和operation的優先級等。是以串行化的operation queue并不等同于GCD中的串行dispatch queue

[java]  view plain copy

  1. // 每次隻能執行一個操作  
  2. queue.maxConcurrentOperationCount = 1;  
  3. // 或者這樣寫  
  4. [queue setMaxConcurrentOperationCount:1];  

六、取消Operations

一旦添加到operation queue,queue就擁有了這個Operation對象并且不能被删除,唯一能做的事情是取消。你可以調用Operation對象的cancel方法取消單個操作,也可以調用operation queue的cancelAllOperations方法取消目前queue中的所有操作。

[java]  view plain copy

  1. // 取消單個操作  
  2. [operation cancel];  
  3. // 取消queue中所有的操作  
  4. [queue cancelAllOperations];  

七、等待Options完成

為了最佳的性能,你應該設計你的應用盡可能地異步操作,讓應用在Operation正在執行時可以去處理其它事情。如果需要在目前線程中處理operation完成後的結果,可以使用NSOperation的waitUntilFinished方法阻塞目前線程,等待operation完成。通常我們應該避免編寫這樣的代碼,阻塞目前線程可能是一種簡便的解決方案,但是它引入了更多的串行代碼,限制了整個應用的并發性,同時也降低了使用者體驗。絕對不要在應用主線程中等待一個Operation,隻能在第二或次要線程中等待。阻塞主線程将導緻應用無法響應使用者事件,應用也将表現為無響應。

[java]  view plain copy

  1. // 會阻塞目前線程,等到某個operation執行完畢  
  2. [operation waitUntilFinished];  

除了等待單個Operation完成,你也可以同時等待一個queue中的所有操作,使用NSOperationQueue的waitUntilAllOperationsAreFinished方法。注意:在等待一個 queue時,應用的其它線程仍然可以往queue中添加Operation,是以可能會加長線程的等待時間。

[java]  view plain copy

  1. // 阻塞目前線程,等待queue的所有操作執行完畢  
  2. [queue waitUntilAllOperationsAreFinished];  

八、暫停和繼續queue

如果你想臨時暫停Operations的執行,可以使用queue的setSuspended:方法暫停queue。不過暫停一個queue不會導緻正在執行的operation在任務中途暫停,隻是簡單地阻止排程新Operation執行。你可以在響應使用者請求時,暫停一個queue來暫停等待中的任務。稍後根據使用者的請求,可以再次調用setSuspended:方法繼續queue中operation的執行

[java]  view plain copy

  1. // 暫停queue  
  2. [queue setSuspended:YES];  
  3. // 繼續queue  
  4. [queue setSuspended:NO];