iOS 多執行緒開發講解
大家都知道,在開發過程中應該儘可能減少使用者等待時間,讓程式儘可能快的完成運算。可是無論是哪種語言開發的程式最終往往轉換成組合語言進而解釋成機器碼來執行。但是機器碼是按順序執行的,一個複雜的多步操作只能一步步按順序逐個執行。改變這種狀況可以從兩個角度出發:對於單核處理器,可以將多個步驟放到不同的執行緒,這樣一來使用者完成UI操作後其他後續任務在其他執行緒中,當CPU空閒時會繼續執行,而此時對於使用者而言可以繼續進行其他操作;對於多核處理器,如果使用者在UI執行緒中完成某個操作之後,其他後續操作在別的執行緒中繼續執行,使用者同樣可以繼續進行其他UI操作,與此同時前一個操作的後續任務可以分散到多個空閒CPU中繼續執行(當然具體排程順序要根據程式設計而定),及解決了執行緒阻塞又提高了執行效率。蘋果從iPad2 開始使用雙核A5處理器(iPhone中從iPhone 4S開始使用),A7中還加入了協處理器,如何充分發揮這些處理器的效能確實值得思考。今天將重點分析iOS多執行緒開發:
- 多執行緒
- 簡介
- iOS多執行緒
- NSThread
- 解決執行緒阻塞問題
- 多執行緒併發
- 執行緒狀態
- 擴充套件-NSObject分類擴充套件
- NSOperation
- NSInvocationOperation
- NSBlockOperation
- 執行緒執行順序
- GCD
- 序列佇列
- 併發佇列
- 其他任務執行方法
- 執行緒同步
- NSLock同步鎖
- @synchronized程式碼塊
- 擴充套件--使用GCD解決資源搶佔問題
- 擴充套件--控制執行緒通訊
- 總結
- 目錄
多執行緒
簡介
當用戶播放音訊、下載資源、進行影象處理時往往希望做這些事情的時候其他操作不會被中斷或者希望這些操作過程中更加順暢。在單執行緒中一個執行緒只能做一件事情,一件事情處理不完另一件事就不能開始,這樣勢必影響使用者體驗。早在單核處理器時期就有多執行緒,這個時候多執行緒更多的用於解決執行緒阻塞造成的使用者等待(通常是操作完UI後用戶不再幹涉,其他執行緒在等待佇列中,CPU一旦空閒就繼續執行,不影響使用者其他UI操作),其處理能力並沒有明顯的變化。如今無論是移動作業系統還是PC、伺服器都是多核處理器,於是“並行運算”就更多的被提及。一件事情我們可以分成多個步驟,在沒有順序要求的情況下使用多執行緒既能解決執行緒阻塞又能充分利用多核處理器執行能力。
下圖反映了一個包含8個操作的任務在一個有兩核心的CPU中建立四個執行緒執行的情況。假設每個核心有兩個執行緒,那麼每個CPU中兩個執行緒會交替執行,兩個CPU之間的操作會並行運算。單就一個CPU而言兩個執行緒可以解決執行緒阻塞造成的不流暢問題,其本身執行效率並沒有提高,多CPU的並行運算才真正解決了執行效率問題,這也正是併發和並行的區別。當然,不管是多核還是單核開發人員不用過多的擔心,因為任務具體分配給幾個CPU運算是由系統排程的,開發人員不用過多關心繫統有幾個CPU。開發人員需要關心的是執行緒之間的依賴關係,因為有些操作必須在某個操作完成完才能執行,如果不能保證這個順序勢必會造成程式問題。
iOS多執行緒
在iOS中每個程序啟動後都會建立一個主執行緒(UI執行緒),這個執行緒是其他執行緒的父執行緒。由於在iOS中除了主執行緒,其他子執行緒是獨立於Cocoa Touch的,所以只有主執行緒可以更新UI介面(新版iOS中,使用其他執行緒更新UI可能也能成功,但是不推薦)。iOS中多執行緒使用並不複雜,關鍵是如何控制好各個執行緒的執行順序、處理好資源競爭問題。常用的多執行緒開發有三種方式:
1.NSThread
2.NSOperation
3.GCD
三種方式是隨著iOS的發展逐漸引入的,所以相比而言後者比前者更加簡單易用,並且GCD也是目前蘋果官方比較推薦的方式(它充分利用了多核處理器的運算效能)。做過.Net開發的朋友不難發現其實這三種開發方式 剛好對應.Net中的多執行緒、執行緒池和非同步呼叫,因此在文章中也會對比講解。
NSThread
NSThread是輕量級的多執行緒開發,使用起來也並不複雜,但是使用NSThread需要自己管理執行緒生命週期。可以使用物件方法+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument直接將操作新增到執行緒中並啟動,也可以使用物件方法- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 建立一個執行緒物件,然後呼叫start方法啟動執行緒。
解決執行緒阻塞問題
在資源下載過程中,由於網路原因有時候很難保證下載時間,如果不使用多執行緒可能使用者完成一個下載操作需要長時間的等待,這個過程中無法進行其他操作。下面演示一個採用多執行緒下載圖片的過程,在這個示例中點選按鈕會啟動一個執行緒去下載圖片,下載完成後使用UIImageView將圖片顯示到介面中。可以看到使用者點選完下載按鈕後,不管圖片是否下載完成都可以繼續操作介面,不會造成阻塞。
<span style="color: green;">// // NSThread實現多執行緒 // MultiThread // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // </span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCMainViewController.h" </span><span style="color: black;">@</span><span style="color: blue;">interface </span><span style="color: black;">KCMainViewController (){ UIImageView *_imageView; } @end @implementation KCMainViewController - (</span><span style="color: blue;">void</span><span style="color: black;">)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 介面佈局 -(</span><span style="color: blue;">void</span><span style="color: black;">)layoutUI{ _imageView =[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame]; _imageView.contentMode=UIViewContentModeScaleAspectFit; [self.view addSubview:_imageView]; UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect]; button.frame=CGRectMake(50, 500, 220, 25); [button setTitle:@</span><span style="color: rgb(163, 21, 21);">"載入圖片" </span><span style="color: black;">forState:UIControlStateNormal]; </span><span style="color: green;">//新增方法 </span><span style="color: black;">[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 將圖片顯示到介面 -(</span><span style="color: blue;">void</span><span style="color: black;">)updateImage:(NSData *)imageData{ UIImage *image=[UIImage imageWithData:imageData]; _imageView.image=image; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 請求圖片資料 -(NSData *)requestData{ </span><span style="color: green;">//對於多執行緒操作建議把執行緒操作放到@autoreleasepool中 </span><span style="color: black;">@autoreleasepool { NSURL *url=[NSURL URLWithString:@</span><span style="color: rgb(163, 21, 21);">"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"</span><span style="color: black;">]; NSData *data=[NSData dataWithContentsOfURL:url]; </span><span style="color: blue;">return </span><span style="color: black;">data; } } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 載入圖片 -(</span><span style="color: blue;">void</span><span style="color: black;">)loadImage{ </span><span style="color: green;">//請求資料 </span><span style="color: black;">NSData *data= [self requestData]; </span><span style="color: green;">/*將資料顯示到UI控制元件,注意只能在主執行緒中更新UI, 另外performSelectorOnMainThread方法是NSObject的分類方法,每個NSObject物件都有此方法, 它呼叫的selector方法是當前呼叫控制元件的方法,例如使用UIImageView呼叫的時候selector就是UIImageView的方法 Object:代表呼叫方法的引數,不過只能傳遞一個引數(如果有多個引數請使用物件進行封裝) waitUntilDone:是否執行緒任務完成執行 */ </span><span style="color: black;">[self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES]; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 多執行緒下載圖片 -(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{ </span><span style="color: green;">//方法1:使用物件方法 //建立一個執行緒,第一個引數是請求的操作,第二個引數是操作方法的引數 // NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil]; // //啟動一個執行緒,注意啟動一個執行緒並非就一定立即執行,而是處於就緒狀態,當系統排程時才真正執行 // [thread start]; //方法2:使用類方法 </span><span style="color: black;">[NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil]; } @end </span>
執行效果:
程式比較簡單,但是需要注意執行步驟:當點選了“載入圖片”按鈕後啟動一個新的執行緒,這個執行緒在演示中大概用了5s左右,在這5s內UI執行緒是不會阻塞的,使用者可以進行其他操作,大約5s之後圖片下載完成,此時呼叫UI執行緒將圖片顯示到介面中(這個過程瞬間完成)。另外前面也提到過,更新UI的時候使用UI執行緒,這裡呼叫了NSObject的分類擴充套件方法,呼叫UI執行緒完成更新。
多個執行緒併發
上面這個演示並沒有演示多個子執行緒操作之間的關係,現在不妨在介面中多載入幾張圖片,每個圖片都來自遠端請求。
大家應該注意到不管是使用+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument、- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 方法還是使用- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait方法都只能傳一個引數,由於更新圖片需要傳遞UIImageView的索引和圖片資料,因此這裡不妨定義一個類儲存圖片索引和圖片資料以供後面使用。
KCImageData.h
<span style="color: green;">// // KCImageData.h // MultiThread // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // </span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);"><Foundation/Foundation.h> </span><span style="color: black;">@</span><span style="color: blue;">interface </span><span style="color: black;">KCImageData : NSObject </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 索引 @</span><span style="color: blue;">property </span><span style="color: black;">(nonatomic,assign) </span><span style="color: blue;">int </span><span style="color: black;">index; </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 圖片資料 @</span><span style="color: blue;">property </span><span style="color: black;">(nonatomic,strong) NSData *data; @end</span>
接下來將建立多個UIImageView並建立多個執行緒用於往UIImageView中填充圖片。
KCMainViewController.m
<span style="color: green;">// // NSThread實現多執行緒 // MultiThread // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // </span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCMainViewController.h" </span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCImageData.h" </span><span style="color: blue;">#define </span><span style="color: black;">ROW_COUNT 5 </span><span style="color: blue;">#define </span><span style="color: black;">COLUMN_COUNT 3 </span><span style="color: blue;">#define </span><span style="color: black;">ROW_HEIGHT 100 </span><span style="color: blue;">#define </span><span style="color: black;">ROW_WIDTH ROW_HEIGHT </span><span style="color: blue;">#define </span><span style="color: black;">CELL_SPACING 10 @</span><span style="color: blue;">interface </span><span style="color: black;">KCMainViewController (){ NSMutableArray *_imageViews; } @end @implementation KCMainViewController - (</span><span style="color: blue;">void</span><span style="color: black;">)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 介面佈局 -(</span><span style="color: blue;">void</span><span style="color: black;">)layoutUI{ </span><span style="color: green;">//建立多個圖片控制元件用於顯示圖片 </span><span style="color: black;">_imageViews=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">]; </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">r=0; r<ROW_COUNT; r++) { </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">c=0; c<COLUMN_COUNT; c++) { UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)]; imageView.contentMode=UIViewContentModeScaleAspectFit; </span><span style="color: green;">// imageView.backgroundColor=[UIColor redColor]; </span><span style="color: black;">[self.view addSubview:imageView]; [_imageViews addObject:imageView]; } } UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect]; button.frame=CGRectMake(50, 500, 220, 25); [button setTitle:@</span><span style="color: rgb(163, 21, 21);">"載入圖片" </span><span style="color: black;">forState:UIControlStateNormal]; </span><span style="color: green;">//新增方法 </span><span style="color: black;">[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 將圖片顯示到介面 -(</span><span style="color: blue;">void</span><span style="color: black;">)updateImage:(KCImageData *)imageData{ UIImage *image=[UIImage imageWithData:imageData.data]; UIImageView *imageView= _imageViews[imageData.index]; imageView.image=image; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 請求圖片資料 -(NSData *)requestData:(</span><span style="color: blue;">int </span><span style="color: black;">)index{ </span><span style="color: green;">//對於多執行緒操作建議把執行緒操作放到@autoreleasepool中 </span><span style="color: black;">@autoreleasepool { NSURL *url=[NSURL URLWithString:@</span><span style="color: rgb(163, 21, 21);">"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"</span><span style="color: black;">]; NSData *data=[NSData dataWithContentsOfURL:url]; </span><span style="color: blue;">return </span><span style="color: black;">data; } } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 載入圖片 -(</span><span style="color: blue;">void</span><span style="color: black;">)loadImage:(NSNumber *)index{ </span><span style="color: green;">// NSLog(@"%i",i); //currentThread方法可以取得當前操作執行緒 </span><span style="color: black;">NSLog(@</span><span style="color: rgb(163, 21, 21);">"current thread:%@"</span><span style="color: black;">,[NSThread currentThread]); </span><span style="color: blue;">int </span><span style="color: black;">i=[index integerValue]; </span><span style="color: green;">// NSLog(@"%i",i);//未必按順序輸出 </span><span style="color: black;">NSData *data= [self requestData:i]; KCImageData *imageData=[[KCImageData alloc]init]; imageData.index=i; imageData.data=data; [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES]; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 多執行緒下載圖片 -(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{ </span><span style="color: green;">//建立多個執行緒用於填充圖片 </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<ROW_COUNT*COLUMN_COUNT; ++i) { </span><span style="color: green;">// [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]]; </span><span style="color: black;">NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]]; thread.name=[NSString stringWithFormat:@</span><span style="color: rgb(163, 21, 21);">"myThread%i"</span><span style="color: black;">,i];</span><span style="color: green;">//設定執行緒名稱 </span><span style="color: black;">[thread start]; } } @end</span>
通過NSThread的currentThread可以取得當前操作的執行緒,其中會記錄執行緒名稱name和編號number,需要注意主執行緒編號永遠為1。多個執行緒雖然按順序啟動,但是實際執行未必按照順序載入照片(loadImage:方法未必依次建立,可以通過在loadImage:中列印索引檢視),因為執行緒啟動後僅僅處於就緒狀態,實際是否執行要由CPU根據當前狀態排程。
從上面的執行效果大家不難發現,圖片並未按順序載入,原因有兩個:第一,每個執行緒的實際執行順序並不一定按順序執行(雖然是按順序啟動);第二,每個執行緒執行時實際網路狀況很可能不一致。當然網路問題無法改變,只能儘可能讓網速更快,但是可以改變執行緒的優先順序,讓15個執行緒優先執行某個執行緒。執行緒優先順序範圍為0~1,值越大優先順序越高,每個執行緒的優先順序預設為0.5。修改圖片下載方法如下,改變最後一張圖片載入的優先順序,這樣可以提高它被優先載入的機率,但是它也未必就第一個載入。因為首先其他執行緒是先啟動的,其次網路狀況我們沒辦法修改:
<span style="color: black;">-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{ NSMutableArray *threads=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">]; </span><span style="color: blue;">int </span><span style="color: black;">count=ROW_COUNT*COLUMN_COUNT; </span><span style="color: green;">//建立多個執行緒用於填充圖片 </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; ++i) { </span><span style="color: green;">// [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]]; </span><span style="color: black;">NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]]; thread.name=[NSString stringWithFormat:@</span><span style="color: rgb(163, 21, 21);">"myThread%i"</span><span style="color: black;">,i];</span><span style="color: green;">//設定執行緒名稱 </span><span style="color: blue;">if</span><span style="color: black;">(i==(count-1)){ thread.threadPriority=1.0; }</span><span style="color: blue;">else</span><span style="color: black;">{ thread.threadPriority=0.0; } [threads addObject:thread]; } </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; i++) { NSThread *thread=threads[i]; [thread start]; } }</span>
執行緒狀態
線上程操作過程中可以讓某個執行緒休眠等待,優先執行其他執行緒操作,而且在這個過程中還可以修改某個執行緒的狀態或者終止某個指定執行緒。為了解決上面優先載入最後一張圖片的問題,不妨讓其他執行緒先休眠一會等待最後一個執行緒執行。修改圖片載入方法如下即可:
<span style="color: black;">-(NSData *)requestData:(</span><span style="color: blue;">int </span><span style="color: black;">)index{ </span><span style="color: green;">//對於多執行緒操作建議把執行緒操作放到@autoreleasepool中 </span><span style="color: black;">@autoreleasepool { </span><span style="color: green;">//對非最後一張圖片載入執行緒休眠2秒 </span><span style="color: blue;">if </span><span style="color: black;">(index!=(ROW_COUNT*COLUMN_COUNT-1)) { [NSThread sleepForTimeInterval:2.0]; } NSURL *url=[NSURL URLWithString:_imageNames[index]]; NSData *data=[NSData dataWithContentsOfURL:url]; </span><span style="color: blue;">return </span><span style="color: black;">data; } }</span>在這裡讓其他執行緒休眠2秒,此時你就會看到最後一張圖片總是第一個載入(除非網速特別差)。
執行緒狀態分為isExecuting(正在執行)、isFinished(已經完成)、isCancellled(已經取消)三種。其中取消狀態程式可以干預設定,只要呼叫執行緒的cancel方法即可。但是需要注意在主執行緒中僅僅能設定執行緒狀態,並不能真正停止當前執行緒,如果要終止執行緒必須線上程中呼叫exist方法,這是一個靜態方法,呼叫該方法可以退出當前執行緒。
假設在圖片載入過程中點選停止按鈕讓沒有完成的執行緒停止載入,可以改造程式如下:
<span style="color: green;">// // NSThread實現多執行緒 // MultiThread // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // </span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCMainViewController.h" </span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCImageData.h" </span><span style="color: blue;">#define </span><span style="color: black;">ROW_COUNT 5 </span><span style="color: blue;">#define </span><span style="color: black;">COLUMN_COUNT 3 </span><span style="color: blue;">#define </span><span style="color: black;">ROW_HEIGHT 100 </span><span style="color: blue;">#define </span><span style="color: black;">ROW_WIDTH ROW_HEIGHT </span><span style="color: blue;">#define </span><span style="color: black;">CELL_SPACING 10 @</span><span style="color: blue;">interface </span><span style="color: black;">KCMainViewController (){ NSMutableArray *_imageViews; NSMutableArray *_imageNames; NSMutableArray *_threads; } @end @implementation KCMainViewController - (</span><span style="color: blue;">void</span><span style="color: black;">)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 介面佈局 -(</span><span style="color: blue;">void</span><span style="color: black;">)layoutUI{ </span><span style="color: green;">//建立多個圖片空間用於顯示圖片 </span><span style="color: black;">_imageViews=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">]; </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">r=0; r<ROW_COUNT; r++) { </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">c=0; c<COLUMN_COUNT; c++) { UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)]; imageView.contentMode=UIViewContentModeScaleAspectFit; </span><span style="color: green;">// imageView.backgroundColor=[UIColor redColor]; </span><span style="color: black;">[self.view addSubview:imageView]; [_imageViews addObject:imageView]; } } </span><span style="color: green;">//載入按鈕 </span><span style="color: black;">UIButton *buttonStart=[UIButton buttonWithType:UIButtonTypeRoundedRect]; buttonStart.frame=CGRectMake(50, 500, 100, 25); [buttonStart setTitle:@</span><span style="color: rgb(163, 21, 21);">"載入圖片" </span><span style="color: black;">forState:UIControlStateNormal]; [buttonStart addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:buttonStart]; </span><span style="color: green;">//停止按鈕 </span><span style="color: black;">UIButton *buttonStop=[UIButton buttonWithType:UIButtonTypeRoundedRect]; buttonStop.frame=CGRectMake(160, 500, 100, 25); [buttonStop setTitle:@</span><span style="color: rgb(163, 21, 21);">"停止載入" </span><span style="color: black;">forState:UIControlStateNormal]; [buttonStop addTarget:self action:@selector(stopLoadImage) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:buttonStop]; </span><span style="color: green;">//建立圖片連結 </span><span style="color: black;">_imageNames=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">]; [_imageNames addObject:@</span><span style="color: black;"> </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<IMAGE_COUNT; i++) { [_imageNames addObject:[NSString stringWithFormat:@</span><span style="color: rgb(163, 21, 21);">"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg"</span><span style="color: black;">,i]]; }</span><span style="color: black;"> } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 將圖片顯示到介面 -(</span><span style="color: blue;">void</span><span style="color: black;">)updateImage:(KCImageData *)imageData{ UIImage *image=[UIImage imageWithData:imageData.data]; UIImageView *imageView= _imageViews[imageData.index]; imageView.image=image; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 請求圖片資料 -(NSData *)requestData:(</span><span style="color: blue;">int </span><span style="color: black;">)index{ </span><span style="color: green;">//對於多執行緒操作建議把執行緒操作放到@autoreleasepool中 </span><span style="color: black;">@autoreleasepool { NSURL *url=[NSURL URLWithString:_imageNames[index]]; NSData *data=[NSData dataWithContentsOfURL:url]; </span><span style="color: blue;">return </span><span style="color: black;">data; } } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 載入圖片 -(</span><span style="color: blue;">void</span><span style="color: black;">)loadImage:(NSNumber *)index{ </span><span style="color: blue;">int </span><span style="color: black;">i=[index integerValue]; NSData *data= [self requestData:i]; NSThread *currentThread=[NSThread currentThread]; </span><span style="color: green;">// 如果當前執行緒處於取消狀態,則退出當前執行緒 </span><span style="color: blue;">if </span><span style="color: black;">(currentThread.isCancelled) { NSLog(@</span><span style="color: rgb(163, 21, 21);">"thread(%@) will be cancelled!"</span><span style="color: black;">,currentThread); [NSThread exit];</span><span style="color: green;">//取消當前執行緒 </span><span style="color: black;">} KCImageData *imageData=[[KCImageData alloc]init]; imageData.index=i; imageData.data=data; [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES]; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 多執行緒下載圖片 -(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{ </span><span style="color: blue;">int </span><span style="color: black;">count=ROW_COUNT*COLUMN_COUNT; _threads=[NSMutableArray arrayWithCapacity:count]; </span><span style="color: green;">//建立多個執行緒用於填充圖片 </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; ++i) { NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]]; thread.name=[NSString stringWithFormat:@</span><span style="color: rgb(163, 21, 21);">"myThread%i"</span><span style="color: black;">,i];</span><span style="color: green;">//設定執行緒名稱 </span><span style="color: black;">[_threads addObject:thread]; } </span><span style="color: green;">//迴圈啟動執行緒 </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; ++i) { NSThread *thread= _threads[i]; [thread start]; } } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 停止載入圖片 -(</span><span style="color: blue;">void</span><span style="color: black;">)stopLoadImage{ </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<ROW_COUNT*COLUMN_COUNT; i++) { NSThread *thread= _threads[i]; </span><span style="color: green;">//判斷執行緒是否完成,如果沒有完成則設定為取消狀態 //注意設定為取消狀態僅僅是改變了執行緒狀態而言,並不能終止執行緒 </span><span style="color: blue;">if </span><span style="color: black;">(!thread.isFinished) { [thread cancel]; } } } @end</span>
執行效果(點選載入大概1秒後點擊停止載入):
使用NSThread在進行多執行緒開發過程中操作比較簡單,但是要控制執行緒執行順序並不容易(前面萬不得已採用了休眠的方法),另外在這個過程中如果列印執行緒會發現迴圈幾次就建立了幾個執行緒,這在實際開發過程中是不得不考慮的問題,因為每個執行緒的建立也是相當佔用系統開銷的。
擴充套件--NSObject分類擴充套件方法
為了簡化多執行緒開發過程,蘋果官方對NSObject進行分類擴充套件(本質還是建立NSThread),對於簡單的多執行緒操作可以直接使用這些擴充套件方法。
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg:在後臺執行一個操作,本質就是重新建立一個執行緒執行當前方法。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait:在指定的執行緒上執行一個方法,需要使用者建立一個執行緒物件。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait:在主執行緒上執行一個方法(前面已經使用過)。
例如前面載入圖多個圖片的方法,可以改為後臺執行緒執行:
<span style="color: black;">-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{ </span><span style="color: blue;">int </span><span style="color: black;">count=ROW_COUNT*COLUMN_COUNT; </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; ++i) { [self performSelectorInBackground:@selector(loadImage:) withObject:[NSNumber numberWithInt:i]]; } }</span>
NSOperation
使用NSOperation和NSOperationQueue進行多執行緒開發類似於C#中的執行緒池,只要將一個NSOperation(實際開中需要使用其子類NSInvocationOperation、NSBlockOperation)放到NSOperationQueue這個佇列中執行緒就會依次啟動。NSOperationQueue負責管理、執行所有的NSOperation,在這個過程中可以更加容易的管理執行緒總數和控制執行緒之間的依賴關係。
NSOperation有兩個常用子類用於建立執行緒操作:NSInvocationOperation和NSBlockOperation,兩種方式本質沒有區別,但是是後者使用Block形式進行程式碼組織,使用相對方便。
NSInvocationOperation
首先使用NSInvocationOperation進行一張圖片的載入演示,整個過程就是:建立一個操作,在這個操作中指定呼叫方法和引數,然後加入到操作佇列。其他程式碼基本不用修改,直接修載入圖片方法如下:
<span style="color: black;">-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{ </span><span style="color: green;">/*建立一個呼叫操作 object:呼叫方法引數 */ </span><span style="color: black;">NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil]; </span><span style="color: green;">//建立完NSInvocationOperation物件並不會呼叫,它由一個start方法啟動操作,但是注意如果直接呼叫start方法,則此操作會在主執行緒中呼叫,一般不會這麼操作,而是新增到NSOperationQueue中 // [invocationOperation start]; //建立操作佇列 </span><span style="color: black;">NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init]; </span><span style="color: green;">//注意新增到操作隊後,佇列會開啟一個執行緒執行此操作 </span><span style="color: black;">[operationQueue addOperation:invocationOperation]; }</span>
NSBlockOperation
下面採用NSBlockOperation建立多個執行緒載入圖片。
<span style="color: green;">// // NSOperation實現多執行緒 // MultiThread // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // </span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCMainViewController.h" </span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCImageData.h" </span><span style="color: blue;">#define </span><span style="color: black;">ROW_COUNT 5 </span><span style="color: blue;">#define </span><span style="color: black;">COLUMN_COUNT 3 </span><span style="color: blue;">#define </span><span style="color: black;">ROW_HEIGHT 100 </span><span style="color: blue;">#define </span><span style="color: black;">ROW_WIDTH ROW_HEIGHT </span><span style="color: blue;">#define </span><span style="color: black;">CELL_SPACING 10 @</span><span style="color: blue;">interface </span><span style="color: black;">KCMainViewController (){ NSMutableArray *_imageViews; NSMutableArray *_imageNames; } @end @implementation KCMainViewController - (</span><span style="color: blue;">void</span><span style="color: black;">)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 介面佈局 -(</span><span style="color: blue;">void</span><span style="color: black;">)layoutUI{ </span><span style="color: green;">//建立多個圖片控制元件用於顯示圖片 </span><span style="color: black;">_imageViews=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">]; </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">r=0; r<ROW_COUNT; r++) { </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">c=0; c<COLUMN_COUNT; c++) { UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)]; imageView.contentMode=UIViewContentModeScaleAspectFit; </span><span style="color: green;">// imageView.backgroundColor=[UIColor redColor]; </span><span style="color: black;">[self.view addSubview:imageView]; [_imageViews addObject:imageView]; } } UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect]; button.frame=CGRectMake(50, 500, 220, 25); [button setTitle:@</span><span style="color: rgb(163, 21, 21);">"載入圖片" </span><span style="color: black;">forState:UIControlStateNormal]; </span><span style="color: green;">//新增方法 </span><span style="color: black;">[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; </span><span style="color: green;">//建立圖片連結 </span><span style="color: black;">_imageNames=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">]; </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<IMAGE_COUNT; i++) { [_imageNames addObject:[NSString stringWithFormat:@</span><span style="color: rgb(163, 21, 21);">"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg"</span><span style="color: black;">,i]]; }</span><span style="color: black;"> } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 將圖片顯示到介面 -(</span><span style="color: blue;">void</span><span style="color: black;">)updateImageWithData:(NSData *)data andIndex:(</span><span style="color: blue;">int </span><span style="color: black;">)index{ UIImage *image=[UIImage imageWithData:data]; UIImageView *imageView= _imageViews[index]; imageView.image=image; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 請求圖片資料 -(NSData *)requestData:(</span><span style="color: blue;">int </span><span style="color: black;">)index{ </span><span style="color: green;">//對於多執行緒操作建議把執行緒操作放到@autoreleasepool中 </span><span style="color: black;">@autoreleasepool { NSURL *url=[NSURL URLWithString:_imageNames[index]]; NSData *data=[NSData dataWithContentsOfURL:url]; </span><span style="color: blue;">return </span><span style="color: black;">data; } } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 載入圖片 -(</span><span style="color: blue;">void</span><span style="color: black;">)loadImage:(NSNumber *)index{ </span><span style="color: blue;">int </span><span style="color: black;">i=[index integerValue]; </span><span style="color: green;">//請求資料 </span><span style="color: black;">NSData *data= [self requestData:i]; NSLog(@</span><span style="color: rgb(163, 21, 21);">"%@"</span><span style="color: black;">,[NSThread currentThread]); </span><span style="color: green;">//更新UI介面,此處呼叫了主執行緒佇列的方法(mainQueue是UI主執行緒) </span><span style="color: black;">[[NSOperationQueue mainQueue] addOperationWithBlock:^{ [self updateImageWithData:data andIndex:i]; }]; } </span><span style="color: blue;">#pragma </span><span style="color: black;">mark 多執行緒下載圖片 -(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{ </span><span style="color: blue;">int </span><span style="color: black;">count=ROW_COUNT*COLUMN_COUNT; </span><span style="color: green;">//建立操作佇列 </span><span style="color: black;">NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init]; operationQueue.maxConcurrentOperationCount=5;</span><span style="color: green;">//設定最大併發執行緒數 //建立多個執行緒用於填充圖片 </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; ++i) { </span><span style="color: green;">//方法1:建立操作塊新增到佇列 // //建立多執行緒操作 // NSBlockOperation *blockOperation=[NSBlockOperation blockOperation