IOS高階程式設計之三:IOS 多執行緒程式設計

橙子瓣發表於2015-05-28

多執行緒的概念在各個作業系統上都會接觸到,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 }

 

相關文章