天天看點

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

一周六早上,小明處于安全考慮,去銀行服務廳申請多一張銀行卡作為手機消費指定數額不多的專用卡。到了銀行,看到大廳坐滿了人,唱K的唱K,念經的念經,嘔奶的嘔奶,彼起此伏,聲聲入耳,直趕清華大學演奏團演奏的《小蘋果》,呀~!其實真實的情況是:每個人都做着椅子上低下頭盯着各自的手機,小明也不例外,找了個角落,浏覽起3016年的新聞。半個小時過去了,40分鐘過去了,一個小時過去!小明等怒了,大喊“嘿嘿嘿,開多一條線程不可以嗎!!!”

“什麼是多一條線程啊?”
談iOS多線程(NSThread、NSOperation、GCD)程式設計

文章大綱

一.基本概念

計算機作業系統都有的基本概念,以下概念簡單方式來描述。

  1. 程序: 一個具有一定獨立功能的程式關于某個資料集合的一次運作活動。可以了解成一個運作中的應用程式。
  2. 線程: 程式執行流的最小單元,線程是程序中的一個實體。
  3. 同步: 隻能在目前線程按先後順序依次執行,不開啟新線程。
  4. 異步: 可以在目前線程開啟多個新線程執行,可不按順序執行。
  5. 隊列: 裝載線程任務的隊形結構。
  6. 并發: 線程執行可以同時一起進行執行。
  7. 串行: 線程執行隻能依次逐一先後有序的執行。

注意:

  • 一個程序可有多個線程。
  • 一個程序可有多個隊列。
  • 隊列可分并發隊列和串行隊列。

二.iOS多線程對比

1. NSThread

每個NSThread對象對應一個線程,真正最原始的線程。

1)優點:NSThread 輕量級最低,相對簡單。

2)缺點:手動管理所有的線程活動,如生命周期、線程同步、睡眠等。

2. NSOperation

自帶線程管理的抽象類。

1)優點:自帶線程周期管理,操作上可更注重自己邏輯。

2)缺點:面向對象的抽象類,隻能實作它或者使用它定義好的兩個子類:NSInvocationOperation 和 NSBlockOperation。

3. GCD

Grand Central Dispatch (GCD)是Apple開發的一個多核程式設計的解決方法。

1)優點:最高效,避開并發陷阱。

2)缺點:基于C實作。

4. 選擇小結

1)簡單而安全的選擇NSOperation實作多線程即可。

2)處理大量并發資料,又追求性能效率的選擇GCD。

3)NSThread本人選擇基本上是在做些小測試上使用,當然也可以基于此造個輪子。

三.場景選擇

  1. 圖檔異步加載。這種常見的場景是最常見也是必不可少的。異步加載圖檔有分成兩種來說明一下。

    第一種,在UI主線程開啟新線程按順序加載圖檔,加載完成重新整理UI。

    第二種,依然是在主線程開啟新線程,順序不定地加載圖檔,加載完成個字重新整理UI。

  2. 創作工具上的異步。 這個跟上邊任務排程道理差不多,隻是為了豐富描述,有助于“舉一反三”效果。如下描述的是app創作小說。

    場景一,app本地創作10個章節内容未成同步伺服器,接着同時發表這10個章節将産生的一系列動作,其中上傳内容,擷取配置設定章節Id,如果背景沒有做處理最好方式做異步按順序執行。

    場景二,app本地創作清單中有3本小說要發表,如果同時發表創作清單中的3本小說,自然考慮并行隊列執行發表。

四.使用方法

第三标題場景選擇内容實作先留下一個懸念。具體實作還是先熟知一下各自的API先。

1. NSThread

1.1)三種實作開啟線程方式:

①.動态執行個體化

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(loadImageSource:) object:imgUrl];
thread.threadPriority = 1;// 設定線程的優先級(0.0 - 1.0,1.0最進階)
[thread start];           

②.靜态執行個體化

[NSThread detachNewThreadSelector:@selector(loadImageSource:) toTarget:self withObject:imgUrl];           

③.隐式執行個體化

[self performSelectorInBackground:@selector(loadImageSource:) withObject:imgUrl];           

有了以上的知識點,可以試探了一下編寫場景選擇中的“圖檔加載”的基本功能了。

1.2)使用這三種方式編寫代碼

//動态建立線程
-(void)dynamicCreateThread{
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(loadImageSource:) object:imgUrl];
    thread.threadPriority = 1;// 設定線程的優先級(0.0 - 1.0,1.0最進階)
    [thread start];
}

//靜态建立線程
-(void)staticCreateThread{
    [NSThread detachNewThreadSelector:@selector(loadImageSource:) toTarget:self withObject:imgUrl];
}

//隐式建立線程
-(void)implicitCreateThread{
    [self performSelectorInBackground:@selector(loadImageSource:) withObject:imgUrl];
}

-(void)loadImageSource:(NSString *)url{
    NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]];
    UIImage *image = [UIImage imageWithData:imgData];
    if (imgData!=nil) {
        [self performSelectorOnMainThread:@selector(refreshImageView:) withObject:image waitUntilDone:YES];
    }else{
        NSLog(@"there no image data");
    }
}

-(void)refreshImageView:(UIImage *)image{
    [self.imageView setImage:image];
}           

1.3)看先效果圖

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

NSThread多線程加載效果

1.4)NSThread的拓展認識

①擷取目前線程

NSThread *current = [NSThread currentThread];           

②擷取主線程

NSThread *main = [NSThread mainThread];           

③暫停目前線程

[NSThread sleepForTimeInterval:2];           

④線程之間通信

//在指定線程上執行操作
[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES]; 
//在主線程上執行操作
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES]; 
//在目前線程執行操作
[self performSelector:@selector(run) withObject:nil];           

顯然動态建立線程多了幾行代碼,其實就是那幾行代碼,如果重複編寫數遍那是一件多麼不爽的事情。首次看來靜态方法創作線程和隐式建立線程顯得比較友善,簡潔。從知識結構來說,講到這裡應該講述一下線程鎖,鑒于并不常用和文章過長就不在此詳細講述,有興趣可以自行查閱。

2. NSOperation

主要的實作方式:結合NSOperation和NSOperationQueue實作多線程程式設計。

  • 執行個體化NSOperation的子類,綁定執行的操作。
  • 建立NSOperationQueue隊列,将NSOperation執行個體添加進來。
  • 系統會自動将NSOperationQueue隊列中檢測取出和執行NSOperation的操作。

2.1)使用NSOperation的子類實作創作線程。

①.NSInvocationOperation建立線程。

NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(loadImageSource:) object:imgUrl];
//[invocationOperation start];//直接會在目前線程主線程執行
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:invocationOperation];           

②.NSBlockOperation建立線程

NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    [self loadImageSource:imgUrl];
}];

NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:blockOperation];           

③.自定義NSOperation子類實作main方法

實作main方法

-(void)main {    
    // Do somthing
}           

建立線程執行個體并添加到隊列中

LoadImageOperation *imageOperation = [LoadImageOperation new];
imageOperation.loadDelegate = self;
imageOperation.imgUrl = imgUrl;

NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:imageOperation];           

2.2)使用這三種方式編寫代碼

建立各個執行個體并添加到隊清單當中

//使用子類NSInvocationOperation
-(void)useInvocationOperation{
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(loadImageSource:) object:imgUrl];
    //[invocationOperation start];//直接會在目前線程主線程執行
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:invocationOperation];

}

//使用子類NSBlockOperation
-(void)useBlockOperation{

    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        [self loadImageSource:imgUrl];
    }];

    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:blockOperation];

}
//使用繼承NSOperation
-(void)useSubclassOperation{

    LoadImageOperation *imageOperation = [LoadImageOperation new];
    imageOperation.loadDelegate = self;
    imageOperation.imgUrl = imgUrl;

    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:imageOperation];
}

-(void)loadImageSource:(NSString *)url{

    NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]];
    UIImage *image = [UIImage imageWithData:imgData];
    if (imgData!=nil) {
        [self performSelectorOnMainThread:@selector(refreshImageView1:) withObject:image waitUntilDone:YES];
    }else{
        NSLog(@"there no image data");
    }

}

-(void)refreshImageView1:(UIImage *)image{
    [self.loadingLb setHidden:YES];
    [self.imageView setImage:image];
}

-(void) loadImageFinish:(UIImage *)image{
    [self.loadingLb setHidden:YES];
    [self.imageView setImage:image];
}           

附自定義NSOperation子類main主要代碼實作

- (void)main {

    if (self.isCancelled) return;

    NSURL *url = [NSURL URLWithString:self.imgUrl];
    NSData *imageData = [NSData dataWithContentsOfURL:url];

    if (self.loadDelegate!=nil&&[self.loadDelegate respondsToSelector:@selector(loadImageFinish:)]) {

        [(NSObject *)self.loadDelegate performSelectorOnMainThread:@selector(loadImageFinish:) withObject:image waitUntilDone:NO];
    }
}           

2.3)看先效果圖

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

NSOperation多線程加載效果

3. GCD多線程

GCD是Apple開發,據說高性能的多線程解決方案。既然這樣,就細說一下這個解決方案。

進過Nsthread和NSOperation的講述和上邊的基礎概念,可以開始組合用起來吧。并發隊列、串行隊列都用起來。

3.1)分發隊列種類(dispatch queue)

①.UI主線程隊列 main queue

dispatch_get_main_queue()           

②.并行隊列global dispatch queue

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)           

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

③.串行隊列serial queues

dispatch_queue_create("minggo.app.com", NULL);           

3.2)6種多線程實作

①背景執行線程建立

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self loadImageSource:imgUrl1];
});           

②UI線程執行(隻是為了測試,長時間加載内容不放在主線程)

dispatch_async(dispatch_get_main_queue(), ^{
    [self loadImageSource:imgUrl1];
});           

③一次性執行(常用來寫單例)

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    [self loadImageSource:imgUrl1];
});           

④并發地執行循環疊代

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
size_t count = 10;
dispatch_apply(count, queue, ^(size_t i) {
    NSLog(@"循環執行第%li次",i);
    [self loadImageSource:imgUrl1];
});           

⑤延遲執行

double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    [self loadImageSource:imgUrl1];
});           

⑥自定義dispatch_queue_t

dispatch_queue_t urls_queue = dispatch_queue_create("minggo.app.com", NULL);
dispatch_async(urls_queue, ^{
    [self loadImageSource:imgUrl1];
});           

3.3)對比多任務執行

異步加載圖檔是大部分app都要面對的問題,那麼加載圖檔是按循序加載完之後才重新整理UI呢?還是不安順序加載UI呢?顯然大部分的希望各自加載各自的圖檔,各自重新整理。以下就是模拟這兩種場景。

①先後執行,加載兩張圖檔為例

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    UIImage *image1 = [self loadImage:imgUrl1];
    UIImage *image2 = [self loadImage:imgUrl2];

    dispatch_async(dispatch_get_main_queue(), ^{
        self.imageview1.image = image1;
        self.imageView2.image = image2;
    });
});           

②并行隊列執行,也是以加載兩張圖檔為例

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);


dispatch_async(queue, ^{

    dispatch_group_t group = dispatch_group_create();

    __block UIImage *image1 = nil;
    __block UIImage *image2 = nil;


    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        image1 = [self loadImage:imgUrl1];
    });

    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        image2 = [self loadImage:imgUrl2];
    });


    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        self.imageview1.image = image1;
        self.imageView2.image = image2;

    });
});           

①中等到兩張圖檔加載完成後一起重新整理,②就是典型的異步并行的例子,不需要理會各自圖檔加載的先後問題,完成加載圖檔重新整理UI即可。從加載圖檔中來說,第①種不太合适使用,但是對于在上邊場景選擇中的創作工具來說有很大的好處,首先得異步進行,然後異步中有得按順序執行幾個任務,比如上傳章節内容。是以,我們可以靈活考慮使用這兩多線程任務執行方式,實作各種場景。

3.4)編碼實作

以上3.3的内容99%代碼一樣,就不提供一個稍微整體的代碼了。看看下邊的效果圖吧。

3.5)效果圖如下

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

GCD多線程加載效果

五.源碼位址

https://github.com/minggo620/iOSMutipleThread.git
如果小明這麼跟銀行櫃台的MM講多線程,會不會。。。“給我滾出去~~”。

繼續閱讀