多執行緒的概念在各個作業系統上都會接觸到,windows、Linux、mac os等等這些常用的作業系統,都支援多執行緒的概念。
當然ios中也不例外,但是執行緒的執行節點可能是我們平常不太注意的。
例如:
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 for(int i = 0 ; i < 100 ; i++) 5 { 6 NSLog(@"===%@===%d" , [NSThread currentThread].name , i); 7 if(i == 20) 8 { 9 // 建立執行緒物件 10 NSThread *thread = [[NSThread alloc]initWithTarget:self 11 selector:@selector(run) object:nil]; 12 // 啟動新執行緒 13 [thread start]; 14 // // 建立並啟動新執行緒 15 // [NSThread detachNewThreadSelector:@selector(run) toTarget:self 16 // withObject:nil]; 17 } 18 } 19 } 20 - (void)run 21 { 22 for(int i = 0 ; i < 100 ; i++) 23 { 24 NSLog(@"-----%@----%d" , [NSThread currentThread].name, i); 25 } 26 }
上面列印的內容每一次都是不同的,什麼意思呢?
當我們建立了4個執行緒後,加上UI主執行緒一共5個執行緒。
新的執行緒在執行start方法之後,並不會立即執行。他們會被cpu隨機的執行,只是間隔非常短,以至於我們感覺上是多個執行緒在同時執行。
所以執行緒有這麼一個特點:執行的隨機性。
但是我們可以設定執行緒的優先順序,讓優先順序更高的執行緒獲得更多的執行機會。
那麼什麼時候要使用多執行緒程式設計呢?
相信有過開發經驗的程式設計師都知道,當我們把程式碼寫完後,程式是一行一行逐行執行程式碼的,當其中一行程式碼需要執行較長時間(例如select一個教複雜的語句或者較多的資料時),那麼程式就會出現卡頓的現象,不會相應使用者的操作。
因為開啟程式後會預設開啟一個主執行緒,即UI執行緒。當處於剛才那種情況時,比如一個windows程式,就會出現程式暫時無響應的提示,好像電腦卡主的感覺,這是非常不好的一種感受。。。。
當我們要避免這種情況的時候,最好的方式就是多執行緒,開啟一個新的執行緒,用來執行一個耗時的操作,執行完成後再讓主執行緒來修改ui頁面(如果需要的話)。
介紹完了執行緒的一些知識,那麼下面來具體看ios中多執行緒的幾種實現方式,主要有一下三種:
1、NSThread :就是剛剛例子中使用的方式,但是使用上比較繁瑣,而且需要控制好資料的同步和非同步問題
2、NSOperation 和 NSOperationQueue : 這種方式程式碼比較簡潔,可讀性強,而且使用佇列的形式管理多個任務,本人比較喜歡
3、使用GCD( Grand Central Dispatch ) :相較於NSThread使用簡單,使用佇列管理任務
一、首先來介紹NSThread
1、建立NSThread的兩種方式
-(id) initWithTarget:(id) target selector:(SEL) selector object:(id) arg:
+(void)detachNewThreadSelector:(SEL) selector toTarget:(id) target withObject:(id) arg:
第二種方式,建立NSThread後會自動啟動
2、NSThread的常用方法
+currentThread : 返回當前正在執行的執行緒物件
3、執行緒的狀態
一開始的例子中提了一下,執行緒建立後,執行了start方法並不是立即就執行了。可能ui執行緒執行了幾毫秒後,cpu才執行它,執行幾毫秒後再執行ui執行緒,但這個過程是隨機發生的。
如果想讓執行緒立即執行,那麼可以讓ui執行緒sleep 1毫秒,這樣cpu就會執行其他可執行的執行緒,可以達到立即執行的效果
1 [NSThread sleepForTimeInterval:0.001];//讓當前執行的執行緒睡眠1毫秒
執行緒正在執行時,呼叫isExecuting方法返回 YES ,執行緒執行完成後呼叫 isFinished 方法就會返回 YES
4、終止子執行緒
執行緒會以一下3種方式之一結束,結束後就處於死亡狀態
1)執行緒執行的方法體執行完成,執行緒正常結束
2)執行過程中出現了錯誤
3)呼叫NSThread 類的 exit 方法來終止當前執行緒
在UI 執行緒中 ,NSThread 並沒有提供方法來結束其他的子執行緒。但是我們可以利用 NSThread 的cancel 方法,執行該方法後, 該執行緒的狀態為 isCancelled = YES,但並不會結束執行緒。
1 NSThread* thread; 2 - (void)viewDidLoad 3 { 4 [super viewDidLoad]; 5 // 建立新執行緒物件 6 thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) 7 object:nil]; 8 // 啟動新執行緒 9 [thread start]; 10 } 11 - (void)run 12 { 13 for(int i = 0 ; i < 100 ; i++) 14 { 15 if([NSThread currentThread].isCancelled) 16 { 17 // 終止當前正在執行的執行緒 18 [NSThread exit]; 19 } 20 NSLog(@"-----%@----%d" , [NSThread currentThread].name, i); 21 // 每執行一次,執行緒暫停0.5秒 22 [NSThread sleepForTimeInterval:0.5]; 23 } 24 } 25 - (IBAction)cancelThread:(id)sender 26 { 27 // 取消thread執行緒,呼叫該方法後,thread的isCancelled方法將會返回NO 28 [thread cancel]; 29 }
利用例子中程式碼的形式,我們就可以達到在UI執行緒中結束其他子執行緒的目的了。
5、執行緒睡眠
要讓執行緒進入阻塞狀態或者睡眠狀態,可以執行sleepXXX格式的方法:
+(void) sleepUntilDate:(NSDate *) aDate : 讓執行緒睡眠,知道aDate那個時間點再醒過來
-(void)sleepForTimeInterval :讓執行緒睡眠多少秒
6、改變執行緒優先順序
NSThread 提供瞭如下幾個方法來獲取和設定執行緒的優先順序
+threadPriority: 獲取當前正在執行的執行緒的優先順序
-threadPriority:獲取執行緒例項的優先順序
+setThreadPriority :(double) priority : 設定當前正在執行的執行緒的優先順序
-setThreadPriority :(double) priority : 設定執行緒例項的優先順序
(double) priority的 取值範圍是0.0~1.0;優先順序越高的執行緒獲得的執行機會越多
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 NSLog(@"UI執行緒的優先順序為:%g" , [NSThread threadPriority]); 5 // 建立第一個執行緒物件 6 NSThread* thread1 = [[NSThread alloc] 7 initWithTarget:self selector:@selector(run) object:nil]; 8 // 設定第一個執行緒物件的名字 9 thread1.name = @"執行緒A"; 10 NSLog(@"執行緒A的優先順序為:%g" , thread1.threadPriority); 11 // 設定使用最低優先順序 12 thread1.threadPriority = 0.0; 13 // 建立第二個執行緒物件 14 NSThread* thread2 = [[NSThread alloc] 15 initWithTarget:self selector:@selector(run) object:nil]; 16 // 設定第二個執行緒物件的名字 17 thread2.name = @"執行緒B"; 18 NSLog(@"執行緒B的優先順序為:%g" , thread2.threadPriority); 19 // 設定使用最高優先順序 20 thread2.threadPriority = 1.0; 21 // 啟動2個執行緒 22 [thread1 start]; 23 [thread2 start]; 24 } 25 - (void)run 26 { 27 for(int i = 0 ; i < 100 ; i++) 28 { 29 NSLog(@"-----%@----%d" , [NSThread currentThread].name, i); 30 } 31 }
二、使用GCD實現多執行緒
GCD簡化了多執行緒的實現,主要有兩個核心概念:
1、佇列:佇列負責管理開發者提交的任務,以先進先出的方式來處理任務。
1)序列佇列:每次只執行一個任務,當前一個任務執行完成後才執行下一個任務
2)並行佇列:多個任務併發執行,所以先執行的任務可能最後才完成(因為具體的執行過程導致)
2、任務:任務就是開發者提供給佇列的工作單元,這些任務將會提交給佇列底層維護的執行緒池,因此這些任務將會以多執行緒的方式執行。
3、建立佇列
1)獲取系統預設的全域性併發佇列:
1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
2) 獲取系統主執行緒關聯的穿行佇列
1 dispatch_queue_t queue = dispatch_get_main_queue();
如果將任務提交給主執行緒關聯的序列佇列,那麼就相當於在程式主執行緒中去執行該任務。
3)建立穿行佇列
1 dispatch_queue_t queue = dispatch_queue_create("queueName", DISPATCH_QUEUE_SERIAL);
4)建立併發佇列
1 dispatch_queue_t queue = dispatch_queue_create("queueName", DISPATCH_QUEUE_CONCURRENT);
5)獲取當前執行程式碼所在佇列
dispatch_get_current_queue,返回一個dispatch_queue_t型別的值
4、提交任務
使用下面的方法將任務以同步或者非同步的方式提交到佇列
1 //將程式碼塊以非同步的方式提交給指定佇列 2 void dispatch_async(dispatch_queue_t queue, dispatch_block_t block); 3 4 //將函式以非同步的方式提交給指定佇列,一般執行函式的方法與執行程式碼塊的方法比,方法名多了一個_f的字尾 5 void dispatch_async_f(dispatch_queue_t queue, void* context, dispatch_function_t work); 6 7 //將程式碼塊以同步的方式提交給指定佇列 8 void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block); 9 10 //將函式以同步的方式提交給指定佇列,一般執行函式的方法與執行程式碼塊的方法比,方法名多了一個_f的字尾 11 void dispatch_sync_f(dispatch_queue_t queue, void* context, dispatch_function_t work);
1 //將程式碼塊以非同步的方式提交給指定佇列,佇列的執行緒池負責在指定時間點 when 之後執行 2 void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block); 3 4 //將函式以非同步的方式提交給指定佇列,佇列的執行緒池負責在指定時間點 when 之後執行 5 void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void* context, dispatch_function_t work); 6 7 //將程式碼塊以非同步的方式提交給指定佇列,佇列的執行緒池將會重複多次執行該任務 8 void dispatch_apply(size_t iterations, dispatch_queue_t queue, void(^block)(size_t)); 9 10 //將函式以非同步的方式提交給指定佇列,佇列的執行緒池將會重複多次執行該任務 11 void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void* context, void(*work)(void*, size_t)); 12 13 //將程式碼塊提交給指定佇列,在應用的某個生命週期內金執行一次 14 void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
下面給出一個以非同步方式向序列佇列、併發佇列新增任務的例項
1 // 定義2個佇列 2 dispatch_queue_t serialQueue; 3 dispatch_queue_t concurrentQueue; 4 - (void)viewDidLoad 5 { 6 [super viewDidLoad]; 7 // 建立序列佇列 8 serialQueue = dispatch_queue_create("fkjava.queue", DISPATCH_QUEUE_SERIAL); 9 // 建立併發佇列 10 concurrentQueue = dispatch_queue_create("fkjava.queue" 11 , DISPATCH_QUEUE_CONCURRENT); 12 } 13 - (IBAction)serial:(id)sender 14 { 15 // 依次將2個程式碼塊提交給序列佇列 16 // 必須等到第1個程式碼塊完成後,才能執行第2個程式碼塊。 17 dispatch_async(serialQueue, ^(void) 18 { 19 for (int i = 0 ; i < 100; i ++) 20 { 21 NSLog(@"%@=====%d" , [NSThread currentThread] , i); 22 } 23 }); 24 dispatch_async(serialQueue, ^(void) 25 { 26 for (int i = 0 ; i < 100; i ++) 27 { 28 NSLog(@"%@------%d" , [NSThread currentThread] , i); 29 } 30 }); 31 } 32 - (IBAction)concurrent:(id)sender 33 { 34 // 依次將2個程式碼塊提交給併發佇列 35 // 兩個程式碼塊可以併發執行 36 dispatch_async(concurrentQueue, ^(void) 37 { 38 for (int i = 0 ; i < 100; i ++) 39 { 40 NSLog(@"%@=====%d" , [NSThread currentThread] , i); 41 } 42 }); 43 dispatch_async(concurrentQueue, ^(void) 44 { 45 for (int i = 0 ; i < 100; i ++) 46 { 47 NSLog(@"%@------%d" , [NSThread currentThread] , i); 48 } 49 }); 50 }
提交同步任務:
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 } 5 - (IBAction)clicked:(id)sender 6 { 7 // 以同步方式先後提交2個程式碼塊 8 dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 9 , ^(void){ 10 for (int i = 0 ; i < 100; i ++) 11 { 12 NSLog(@"%@=====%d" , [NSThread currentThread] , i); 13 [NSThread sleepForTimeInterval:0.1]; 14 } 15 }); 16 // 必須等第一次提交的程式碼塊執行完成後,dispatch_sync()函式才會返回, 17 // 程式才會執行到這裡,才能提交第二個程式碼塊。 18 dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 19 , ^(void){ 20 for (int i = 0 ; i < 100; i ++) 21 { 22 NSLog(@"%@-----%d" , [NSThread currentThread] , i); 23 [NSThread sleepForTimeInterval:0.1]; 24 } 25 }); 26 }
多次執行的任務:
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 } 5 - (IBAction)clicked:(id)sender 6 { 7 // 控制程式碼塊執行5次 8 dispatch_apply(5 9 , dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 10 // time形參代表當前正在執行第幾次 11 , ^(size_t time) 12 { 13 NSLog(@"===執行【%lu】次===%@" , time 14 , [NSThread currentThread]); 15 }); 16 }
只執行一次的任務
1 @implementation FKViewController 2 - (void)viewDidLoad 3 { 4 [super viewDidLoad]; 5 } 6 - (IBAction)clicked:(id)sender 7 { 8 static dispatch_once_t onceToken; 9 dispatch_once(&onceToken, ^{ 10 NSLog(@"==執行程式碼塊=="); 11 // 執行緒暫停3秒 12 [NSThread sleepForTimeInterval:3]; 13 }); 14 }
三、使用NSOperation 和 NSOPerationQueue 實現多執行緒
和GCD差不多,也是有佇列和任務的概念
NSOperationQueue:代表一個先進先出的佇列,負責管理系統提交的多個NSOperation。底層維護一個執行緒池,會按順序啟動執行緒來執行提交給佇列的NSOperation
NSOperation:代表多執行緒任務。一般不直接使用NSOperation,而是使用NSOperation的子類。或者使用NSInvocationOperation和NSBlockOperation(這兩個類繼承自NSOperation);
1、NSOperation的使用
NSOperation 的使用相較於GCD是物件導向的,OC實現的,而GCD應該是C實現的(看函式的定義和使用)。
使用NSOperation 只需兩步:
1)建立 NSOperationQueue 佇列,並未該佇列設定相關屬性
2)建立 NSOperation 子類物件,並將該物件提交給 NSOperationQueue 佇列,該佇列將會按順序依次啟動每個 NSOperation。
2、NSOperationQueue的常用方法:
1 +currentQueue //類方法,返回執行當前NSOperation的NSOperationQueue佇列 2 3 +mainQueue //返回系統主執行緒的NSOperationQueue佇列 4 5 -(void) addOperation:(NSOperation *) operation //將operation新增到NSOperationQueue佇列中 6 7 -(void) addOperations:(NSArray *) ops waitUnitlFinished:(BOLL) wait //將NSArray中包含的所有NSOperation新增到NSOperationQueue。如果第二個引數指定為YES,將會阻塞當前執行緒,直到提交的所有NSOperation執行完成。如果第二個引數為NO,該方法立即返回,NSArray包含的NSOperation將以非同步方式執行,不會阻塞當前執行緒。 8 9 - operations //只讀屬性,返回該NSOperationQueue管理的所有NSOperation 10 -operationCount //只讀屬性,返回該NSOperationQueue管理的所有NSOperation數量 11 12 -cancelAllOperations: //取消NSOperationQueue佇列中所有正在排隊和執行的NSOperation 13 14 -waitUntilAllOperationsAreFinished://阻塞當前執行緒,直到該NSOperationQueue中所有排隊和執行的NSOperation執行完成再接觸阻塞 15 16 -(NSInteger) maxConcurrentOperationCount://返回該佇列最大支援多少個併發執行緒 17 18 -setMaxConcurrentOperationCount:(NSInteger) count //設定該佇列最大支援多少個併發執行緒 19 20 -setSuspended:(BOOL) suspend: //設定NSOperationQueue是否已經暫停排程正在排隊的NSOperation 21 22 -(BOLL) isSuspended: //返回NSOperationQueue是否已經暫停排程正在排隊的NSOperation
3、使用NSInvocationOperation 和 NSBlockOperation
NSInvocationOperation 和 NSBlockOperation 繼承自 NSOperation,所以可以直接使用,用於封裝需要非同步執行的任務。
使用它們實現圖片非同步下載:
1 NSOperationQueue* queue; 2 - (void)viewDidLoad 3 { 4 [super viewDidLoad]; 5 queue = [[NSOperationQueue alloc]init]; 6 // 設定該佇列最多支援10條併發執行緒 7 queue.maxConcurrentOperationCount = 10; 8 } 9 - (IBAction)clicked:(id)sender 10 { 11 NSString* url = @"http://www.......jpg"; 12 // 以傳入的程式碼塊作為執行體,建立NSOperation 13 NSBlockOperation* operation = [NSBlockOperation 14 blockOperationWithBlock:^{ 15 // 從網路獲取資料 16 NSData *data = [[NSData alloc] 17 initWithContentsOfURL:[NSURL URLWithString:url]]; 18 // 將網路資料初始化為UIImage物件 19 UIImage *image = [[UIImage alloc]initWithData:data]; 20 if(image != nil) 21 { 22 // 在主執行緒中執行updateUI:方法 23 [self performSelectorOnMainThread:@selector(updateUI:) 24 withObject:image waitUntilDone:YES]; 25 } 26 else 27 { 28 NSLog(@"---下載圖片出現錯誤---"); 29 } 30 }]; 31 // 將NSOperation新增給NSOperationQueue 32 [queue addOperation:operation]; 33 } 34 -(void)updateUI:(UIImage*) image 35 { 36 self.iv.image = image; 37 }
1 NSOperationQueue* queue; 2 - (void)viewDidLoad 3 { 4 [super viewDidLoad]; 5 queue = [[NSOperationQueue alloc]init]; 6 // 設定該佇列最多支援10條併發執行緒 7 queue.maxConcurrentOperationCount = 10; 8 } 9 - (IBAction)clicked:(id)sender 10 { 11 NSString* url = @"http://www.......jpg"; 12 // 以self的downloadImageFromURL:方法作為執行體,建立NSOperation 13 NSInvocationOperation* operation = [[NSInvocationOperation alloc] 14 initWithTarget:self selector:@selector(downloadImageFromURL:) 15 object:url]; 16 // 將NSOperation新增給NSOperationQueue 17 [queue addOperation:operation]; 18 } 19 20 // 定義一個方法作為執行緒執行體。 21 -(void)downloadImageFromURL:(NSString *) url 22 { 23 // 從網路獲取資料 24 NSData *data = [[NSData alloc] 25 initWithContentsOfURL:[NSURL URLWithString:url]]; 26 // 將網路資料初始化為UIImage物件 27 UIImage *image = [[UIImage alloc]initWithData:data]; 28 if(image != nil) 29 { 30 // 在主執行緒中執行updateUI:方法 31 [self performSelectorOnMainThread:@selector(updateUI:) 32 withObject:image waitUntilDone:YES]; 33 } 34 else 35 { 36 NSLog(@"---下載圖片出現錯誤---"); 37 } 38 } 39 -(void)updateUI:(UIImage*) image 40 { 41 self.iv.image = image; 42 }
4、自定義NSOperation 的子類
建立 NSOperation 的子類,需要重寫一個方法:-(void) main,該方法的方法體將作為 NSOperationQueue 完成的任務
下面自定義一個NSOperation 子類來實現下載圖片的功能
1 @interface MyDownImageOperation : NSOperation 2 @property (nonatomic , strong) NSURL* url; 3 @property (nonatomic , weak) UIImageView* imageView; 4 - (id)initWithURL:(NSURL*)url imageView:(UIImageView*)iv; 5 @end
1 @implementation MyDownImageOperation 2 - (id)initWithURL:(NSURL*)url imageView:(UIImageView*)iv 3 { 4 self = [super init]; 5 if (self) { 6 _imageView = iv; 7 _url = url; 8 } 9 return self; 10 } 11 // 重寫main方法,該方法將作為執行緒執行體 12 - (void)main 13 { 14 // 從網路獲取資料 15 NSData *data = [[NSData alloc] 16 initWithContentsOfURL:self.url]; 17 // 將網路資料初始化為UIImage物件 18 UIImage *image = [[UIImage alloc]initWithData:data]; 19 if(image != nil) 20 { 21 // 在主執行緒中執行updateUI:方法 22 [self performSelectorOnMainThread:@selector(updateUI:) 23 withObject:image waitUntilDone:YES]; // ① 24 } 25 else 26 { 27 NSLog(@"---下載圖片出現錯誤---"); 28 } 29 } 30 -(void)updateUI:(UIImage*) image 31 { 32 self.imageView.image = image; 33 }
viewController程式碼:
1 NSOperationQueue* queue; 2 - (void)viewDidLoad 3 { 4 [super viewDidLoad]; 5 queue = [[NSOperationQueue alloc]init]; 6 // 設定該佇列最多支援10條併發執行緒 7 queue.maxConcurrentOperationCount = 10; 8 } 9 - (IBAction)clicked:(id)sender 10 { 11 // 定義要載入的圖片的URL 12 NSURL* url = [NSURL URLWithString:@"http://www.crazyit.org/logo.jpg"]; 13 // 建立FKDownImageOperation物件 14 MyDownImageOperation* operation = [[MyDownImageOperation alloc] 15 initWithURL:url imageView:self.iv]; 16 // 將NSOperation的子類的例項提交給NSOperationQueue 17 [queue addOperation:operation]; 18 }