1. 程式人生 > >iOS學習筆記1-多執行緒GCD與block

iOS學習筆記1-多執行緒GCD與block

學習IOS也有兩三個月了,今天來總結下學習GCD的知識點,希望大家多多指教:

1.GCD簡介以及block

GCD:Grand Central Dispatch或者GCD,是一套low level API,提供了一種新的方法來進行併發程式編寫。從基本功能上講,GCD有點像NSOperationQueue,他們都允許程式將任務切分為多個單一任務然後提交至工作佇列來併發地或者序列地執行。GCD比之NSOpertionQueue更底層更高效,並且它不是Cocoa框架的一部分。使用GCD比使用NSOpertionQueue方便;
Grand Central Dispatch (GCD)是Apple開發的一個多核程式設計的解決方法。該方法在Mac OS X 10.6中首次推出,並隨後被引入到了iOS4.0中。GCD是一個替代諸如NSThread, NSOperationQueue, NSInvocationOperation等技術的很高效和強大的技術,它看起來象就其它語言的閉包(Closure)一樣,但蘋果把它叫做blocks。
block:塊是C語言的一種擴充套件。先給個典型的程式碼:

^(void){
    NSLog(@"Program is funning\n");
};

由上例,典型的block指的是以插入“^”為開頭為標識的程式碼塊,^後面跟的(void)表示塊所需要的引數列表;
當然,我們可以定義一個變數來表示這個block,就像是函式指標的感覺(有兩種方法):
(1)方法1:如下例:

void (^printMessage) (void)=
^(void){
    NSLog(@"Program is funning\n");
};

等號左邊表示的是printMessage指向一個沒有引數和返回值的塊指標,第一個void是返回值(和C語言函式語法一致),第二個void指的是傳入的引數(類比於函式的形參);
執行一個變數引用的塊,與函式呼叫方法一致:
printMessage();
(2)方法2:如下例:(更加常用的方法)

dispatch_block_t block = ^{
                //do something
                        };

呼叫方法一致:

block();

關於block變數:

(1)對於區域性自動變數:在Block中只讀。Block定義時copy變數的值,在Block中作為常量使用,所以即使變數的值在Block外改變,也不影響他在Block中的值。

int base = 100;
BlkSum sum = ^ long (int a, int b) {
  // base++; 編譯錯誤,只讀
  return base + a + b;
};
base
= 0; printf("%ld\n",sum(1,2)); // 這裡輸出是103,而不是3

注意:,可以從上面看到,這是block只會從定義該block之前取值,對於base=0這條語句是忽略的;
(2)static變數、全域性變數。如果把上個例子的base改成全域性的、或static。Block就可以對他進行讀寫了。因為全域性變數或靜態變數在記憶體中的地址是固定的,Block在讀取該變數值的時候是直接從其所在記憶體讀出,獲取到的是最新值,而不是在定義時copy的常量。

static int base = 100;
BlkSum sum = ^ long (int a, int b) {
  base++;
  return base + a + b;
};
base = 0;
printf("%d\n", base);
printf("%ld\n",sum(1,2)); // 這裡輸出是3,而不是103
printf("%d\n", base);

輸出結果是0 4 1,表明Block外部對base的更新會影響Block中的base的取值,同樣Block對base的更新也會影響Block外部的base值。
(3)對於block型的變數:被__block修飾的變數稱作Block變數。 基本型別的Block變數等效於全域性變數、或靜態變數。

也就是說,經過__block修飾的變數是不會隨著物件的重新生成而改變地址,而是類似於靜態全域性變數一樣,從程式開始到結束,僅有一個變數地址,這一點在程式設計時經常出錯;
(4)block會對其內部的變數進行強引用,這是可能造成記憶體迴圈引用的問題。
注意:block在GCD中是必需的,所以要重點掌握。

2.GCD語法

1.三大概念:(非常重要)

(1)佇列和任務
佇列:用來存放任務;
任務:用來執行什麼操作,簡單理解可以理解為自定義的一個函式;
(2)同步和非同步
同步:只能在當前執行緒中執行任務,不具備開啟新執行緒的能力;
非同步:可以在新的執行緒中執行任務,具備開啟新執行緒的能力;
說明:兩者最大的區別就是開不開執行緒,可以這樣理解,對於一件事情,同步是會只在一個執行緒一直執行下去,而對於非同步,則會一需要執行該任務,開一條新的執行緒,把這個任務交給該執行緒執行下去,立即返回原來的執行緒,這樣子達到了非同步的效果;
(3)併發和序列佇列:
併發:可以讓多個任務併發(同時)執行(自動開啟多個執行緒同時執行任務),併發功能只有在非同步(dispatch_async)函式下才有效;
序列:讓序列佇列內任務一個接著一個地執行(序列佇列內一個任務執行完畢後,再執行下一個任務);但對於每個序列佇列,是併發進行的
這裡寫圖片描述

2.使用方法:

(1)第一步,確定自己要執行的任務,也就是編寫好任務對應的函式;
(2)第二步,將寫好的任務新增到佇列中,GCD會自動將佇列中得任務取出,放到對應的執行緒中執行,注意,這裡系統是自動放到執行緒中執行;
3.實際使用方法(示例程式碼):
(1)第一步,先定義一個佇列:
*全域性的併發佇列 : 可以讓任務併發執行

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

注: GCD預設已經提供了全域性的併發佇列,供整個應用使用,不需要手動建立,每次一執行程式就存在的,我們要做的只是使用dispatch_get_global_queue函式獲得全域性的併發佇列;
使用格式:

dispatch_queue_t dispatch_get_global_queue(
    dispatch_queue_priority_t priority, // 佇列的優先順序
    unsigned long flags); // 此引數暫時無用,用0即可
 ```

關於priority,系統提供了4種優先順序:
  ```
   #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
   #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 預設(中)
   #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
   #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 

關於DISPATCH_QUEUE_PRIORITY_DEFAULT引數的理解:一般都選這個引數就可以了;
*自己建立的序列佇列 : 讓任務一個接著一個執行

dispatch_queue_t queue = dispatch_queue_create("cn.heima.queue", NULL);

*主佇列 : 讓任務在主執行緒執行,也就是介面所在的執行緒

dispatch_queue_t queue = dispatch_get_main_queue();//這個就是主執行緒

(2)第二步,將佇列放入程式執行,如下:
1> 同步執行 : 不具備開啟新執行緒的能力

dispatch_sync...

注意
使用sync函式往當前序列佇列中新增任務,會卡住當前的序列佇列
2> 非同步執行 : 具備開啟新執行緒的能力,但不代表每次都開新的執行緒

dispatch_async...

例子:

   dispatch_queue_t queue = dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
   dispatch_async(queue, ^{
    printf("this is a block!\n"); 
    }); 

上面這個例子就是簡單地一個GCD的使用程式碼,先定義一個queue,可以如上選擇全域性佇列,也可以選擇其他佇列,然後根據是同步還是非同步選擇dispatch_async或者dispatch_sync將佇列放在該函式執行,一執行dispatch_async或者dispatch_sync,系統自動管理執行緒去執行該函式。
GCD的用法很多,更加常見的搭配使用方法如下:

  1. dispatch_async + 全域性併發佇列
  2. dispatch_async + 自己建立的序列佇列

同時,執行緒與主執行緒之間的通訊也是常見的GCD用處之一,一般使用格式如下(經典用法):

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   // 執行耗時的非同步操作...


   dispatch_async(dispatch_get_main_queue(), ^{
       // 回到主執行緒,執行UI重新整理操作
   });
});

這也就是不阻塞主執行緒常用的方法,注意,只有非同步才能達到這個效果(注意不要弄錯)。

關於一次性執行程式碼

    //在當前執行緒執行任務,能保證某段程式碼在程式執行過程中只被執行1次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"once");
        NSLog(@"%@",[NSThread currentThread]);
    });

關於延時執行

 //方法1
    //在當前執行緒睡眠3秒,如果在主執行緒,則會卡住介面;不推薦使用
    //    [NSThread sleepForTimeInterval:3];

    //方法2
    //一旦定製好延時延誤後,不會卡住當前執行緒
    //誰呼叫這條函式,n秒後回到該執行緒繼續執行run方法
    //    [self performSelector:@selector(run ) withObject:nil afterDelay:3];

    //方法3(放在主執行緒延時)
    //GCD
    //放在主執行緒延時,傳入的是dispatch_get_main_queue()主佇列
    //    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    //        NSLog(@"task");
    //        NSLog(@"run %@",[NSThread currentThread]);
    //
    //    });

    //方法3(放在主執行緒延時)
    //不放在主執行緒延時,會自動開執行緒,3秒後在新執行緒執行任務函式
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), queue, ^{

        NSLog(@"task");
        NSLog(@"run %@",[NSThread currentThread]);

    });

關於佇列組:

有這麼1種需求
首先:分別非同步執行2個耗時的操作
其次:等2個非同步操作都執行完畢後,再回到主執行緒執行操作

如果想要快速高效地實現上述需求,可以考慮用佇列組
dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 執行1個耗時的非同步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 執行1個耗時的非同步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 等前面的非同步操作都執行完畢後,回到主執行緒...
});

參考例子:
同時從網站上面下載兩張圖片,將下載過程放在佇列組,兩張都下載完成後執行合併圖片操作,最後在主執行緒重新整理圖片:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //建立一個佇列組
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    __block UIImage * image1 = nil;
    __block UIImage * image2 = nil;
    dispatch_group_async(group, queue, ^{
    //執行一個耗時的非同步操作

        //下載第一張
        NSURL *url1 = [NSURL URLWithString:@"http://g.hiphotos.baidu.com/image/pic/item/f2deb48f8c5494ee460de6182ff5e0fe99257e80.jpg"];
        NSData *data1 = [NSData dataWithContentsOfURL:url1];
        image1 = [UIImage imageWithData:data1];

    });

    dispatch_group_async(group, queue, ^{
       //執行一個耗時的非同步操作

        //下載第二張
        NSURL *url2 = [NSURL URLWithString:@"http://su.bdimg.com/static/superplus/img/logo_white_ee663702.png"];
        NSData *data2 = [NSData dataWithContentsOfURL:url2];
        image2 = [UIImage imageWithData:data2];

    });


    //等2個非同步操作都執行完畢後,再回到主執行緒執行操作
    dispatch_group_notify(group, queue, ^{
        //合併圖片
        //開啟一個位圖上下文
        UIGraphicsBeginImageContextWithOptions(image1.size, NO, 0.0);

        //繪製第一張圖片
        [image1 drawInRect:CGRectMake(0, 0, image1.size.width, image1.size.height)];

        //繪製第二章圖片
        [image2 drawInRect:CGRectMake(0, 0, image2.size.width*0.5, image2.size.height*0.5)];

        //得到上下文中的圖片
        UIImage *fullImgae = UIGraphicsGetImageFromCurrentImageContext();
        //結束上下文
        UIGraphicsEndImageContext();

        //回到主執行緒顯示圖片
        dispatch_async(dispatch_get_main_queue(), ^{
            self.iamgeView.image = fullImgae;
        });

    });


}

我也在學習iOS,希望能和大家一起共同進步,有問題可以留言或者私信;