iOS Block用法和實現原理

dyydp 7年前發布 | 12K 次閱讀 iOS開發 移動開發 Objective-C

《Objective-C高級編程》是一本有趣又難懂的書,全書就講了引用計數、Block、GCD三個概念,有趣是因為講原理、實現的部分是其它iOS專業書籍里少有的。然而每個章節不讀個三五遍還是比較難理解貫通的。本文針對其中的Block部分做些簡單的筆記記錄,講述Block的用法和部分實現原理,詳細解說從原書中尋。

Block概要

Block:帶有 自動變量匿名函數

匿名函數:沒有函數名的函數,一對{}包裹的內容是匿名函數的作用域。

自動變量:棧上聲明的一個變量不是靜態變量和全局變量,是不可以在這個棧內聲明的匿名函數中使用的,但在Block中卻可以。

雖然使用Block不用聲明類,但是Block提供了類似Objective-C的類一樣可以通過成員變量來 保存作用域外變量值 的方法,那些在Block的一對{}里使用到但卻是在{}作用域以外聲明的變量,就是Block截獲的自動變量。

Block常規概念

Block語法

Block表達式語法:

^ 返回值類型 (參數列表) {表達式}

例如:

^ int (int count) {
        return count + 1;
    };

其中,可省略部分有:

  • 返回類型,例:
    ^ (int count) {
          return count + 1;
      };
  • 參數列表為空,則可省略,例:
    ^ {
          NSLog(@"No Parameter");
      };
    即最簡模式語法為:

    ^ {表達式}

Block類型變量

聲明Block類型變量語法:

返回值類型 (^變量名)(參數列表) = Block表達式

例如,如下聲明了一個變量名為blk的Block:

int (^blk)(int) = ^(int count) {
        return count + 1;
    };

當Block類型變量作為函數的參數時,寫作:

- (void)func:(int (^)(int))blk {
    NSLog(@"Param:%@", blk);
}

借助typedef可簡寫:

typedef int (^blk_k)(int);

- (void)func:(blk_k)blk {
    NSLog(@"Param:%@", blk);
}

Block類型變量作返回值時,寫作:

- (int (^)(int))funcR {
    return ^(int count) {
        return count ++;
    };
}

借助typedef簡寫:

typedef int (^blk_k)(int);

- (blk_k)funcR {
    return ^(int count) {
        return count ++;
    };
}

截獲自動變量值

Block表達式可截獲所使用的自動變量的值。

截獲:保存自動變量的 瞬間值

因為是“瞬間值”,所以聲明Block之后,即便在Block外修改自動變量的值,也不會對Block內截獲的自動變量值產生影響。

例如:

int i = 10;
    void (^blk)(void) = ^{
        NSLog(@"In block, i = %d", i);
    };
    i = 20;//Block外修改變量i,也不影響Block內的自動變量
    blk();//i修改為20后才執行,打印: In block, i = 10
    NSLog(@"i = %d", i);//打印:i = 20

__block說明符號

自動變量截獲的值為Block聲明時刻的瞬間值,保存后就不能改寫該值,如需對自動變量進行重新賦值,需要在變量聲明前附加__block說明符,這時該變量稱為__block變量。

例如:

__block int i = 10;//i為__block變量,可在block中重新賦值
    void (^blk)(void) = ^{
        NSLog(@"In block, i = %d", i);
    };
    i = 20;
    blk();//打印: In block, i = 20
    NSLog(@"i = %d", i);//打印:i = 20

自動變量值為一個對象情況

當自動變量為一個類的 對象 ,且沒有使用__block修飾時,雖然不可以在Block內對該變量進行重新賦值,但可以修改該對象的屬性。

如果該對象是個Mutable的對象,例如NSMutableArray,則還可以在Block內對NSMutableArray進行元素的增刪:

NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:@"1", @"2",nil ];
    NSLog(@"Array Count:%ld", array.count);//打印Array Count:2
    void (^blk)(void) = ^{
        [array removeObjectAtIndex:0];//Ok
        //array = [NSNSMutableArray new];//沒有__block修飾,編譯失敗!
    };
    blk();
    NSLog(@"Array Count:%ld", array.count);//打印Array Count:1

Block實現原理

使用Clang

Block實際上是作為極普通的 C語言源碼 來處理的:含有Block語法的源碼首先被轉換 成C語言編譯器能處理的源碼 ,再作為普通的C源代碼 進行編譯

使用LLVM編譯器的clang命令可將含有Block的Objective-C代碼轉換成C++的源代碼,以探查其具體實現方式:

clang -rewrite-objc 源碼文件名

注:如果使用該命令報錯: ’UIKit/UIKit.h’ file not found

Block結構

使用Block的時候,編譯器對Block語法進行了怎樣的轉換?

int main() {
    int count = 10;
    void (^ blk)() = ^(){
        NSLog(@"In Block:%d", count);
    };
    blk();
}

如上所示的最簡單的Block使用代碼,經clang轉換后,可得到以下幾個部分(有代碼刪減和注釋添加):

static void __main_block_func_0(
    struct __main_block_impl_0 *__cself) {
    int count = __cself->count; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_d2f8d2_mi_0, 
    count);
}

這是一個函數的實現,對應Block中{}內的內容,這些內容被當做了C語言函數來處理,函數參數中的 __cself 相當于Objective-C中的self。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc; //描述Block大小、版本等信息
  int count;
  //構造函數函數
  __main_block_impl_0(void *fp,
          struct __main_block_desc_0 *desc,
          int _count,
          int flags=0) : count(_count) {
    impl.isa = &_NSConcreteStackBlock; //在函數棧上聲明,則為_NSConcreteStackBlock
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0即為 main()函數棧上的Block結構體 ,其中的__block_impl結構體聲明如下:

struct __block_impl {
  void *isa;//指明對象的Class
  int Flags;
  int Reserved;
  void *FuncPtr;
};

__block_impl結構體,即為Block的結構體,可理解為 Block的類結構

再看下main()函數翻譯的內容:

int main() {
    int count = 10;
    void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

去除掉復雜的類型轉化,可簡寫為:

int main() {
    int count = 10;
    sturct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0,         //函數指針
                                                           &__main_block_desc_0_DATA)); //Block大小、版本等信息

    (*blk->FuncPtr)(blk);   //調用FuncPtr指向的函數,并將blk自己作為參數傳入
}

由此,可以看出, Block也是Objective-C中的對象

Block有三種類(即__block_impl的 isa 指針指向的值,根據Block對象創建時 所處數據區不同 而進行區別:

  • _NSConcreteStackBlock :在棧上創建的Block對象
  • _NSConcreteMallocBlock :在堆上創建的Block對象
  • _NSConcreteGlobalBlock :全局數據區的Block對象

如何截獲自動變量

上部分介紹了 Block的結構 ,和 作為匿名函數的調用機制 ,那 自動變量截獲 是發生在什么時候呢?

觀察上節代碼中 __main_block_impl_0 結構體(main棧上Block的結構體)的構造函數可以看到,棧上的變量count以參數的形式傳入到了這個構造函數中,此處即為 變量的自動截獲

因此可以這樣理解: __block_impl 結構體已經可以代表Block類了,但在棧上又聲明了 __main_block_impl_0 結構體,對 __block_impl 進行 封裝 后才來表示棧上的 Block類 ,就是為了獲取Block中使用到的棧上聲明的變量( 棧上沒在Block中使用的變量不會被捕獲 ),變量被保存在Block的結構體實例中。

所以在blk()執行之前,棧上簡單數據類型的count無論發生什么變化,都不會影響到Block以參數形式傳入而捕獲的值。但這個變量是指向對象的指針時,是可以修改這個對象的屬性的,只是不能為變量重新賦值。

Block的存儲域

上文已提到,根據Block創建的位置不同,Block有三種類型,創建的Block對象分別會存儲到棧、堆、全局數據區域。

void (^blk)(void) = ^{
    NSLog(@"Global Block");
};

int main() {
    blk();
    NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
}

像上面代碼塊中的全局blk自然是存儲在全局數據區,但注意在 函數棧上 創建的blk,如果 沒有截獲自動變量 ,Block的結構實例還是會被設置在程序的 全局數據區,而非棧上

int main() {
    void (^blk)(void) = ^{//沒有截獲自動變量的Block
        NSLog(@"Stack Block");
    };
    blk();
    NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__

    int i = 1;
    void (^captureBlk)(void) = ^{//截獲自動變量i的Block
        NSLog(@"Capture:%d", i);
    };
    captureBlk();
    NSLog(@"%@",[captureBlk class]);//打印:__NSMallocBlock__
}

可以看到截獲了自動變量的Block打印的類是 NSGlobalBlock ,表示存儲在全局數據區。

但為什么捕獲自動變量的Block打印的類卻是設置在堆上的 NSMallocBlock ,而非棧上的 NSStackBlock ?這個問題稍后解釋。

Block復制

配置在棧上的Block,如果其所屬的棧作用域結束,該Block就會被廢棄,對于超出Block作用域仍需使用Block的情況,Block提供了 將Block從棧上復制到堆上的方法 來解決這種問題,即便Block棧作用域已結束,但被拷貝到堆上的Block還可以繼續存在。

復制到堆上的Block, 將_NSConcreteMallocBlock 類對象寫入Block結構體實例的成員變量isa:

impl.isa = &_NSConcreteMallocBlock;

在ARC有效時,大多數情況下編譯器會進行判斷,自動生成將Block從棧上復制到堆上的代碼,以下幾種情況 棧上的Block會自動復制到堆上

  • 調用Block的copy方法
  • 將Block作為函數返回值時
  • 將Block賦值給__strong修改的變量時
  • 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數時

其它時候向方法的參數中傳遞Block時,需要手動調用copy方法復制Block。

上一節的棧上截獲了自動變量i的Block之所以在棧上創建,卻是 NSMallocBlock_ 類,就是因為這個Block對象賦值給了 _strong修飾的變量 captureBlk(

strong是ARC下對象的默認修飾符)。

因為上面四條規則,在ARC下其實很少見到_NSConcreteStackBlock類的Block,大多數情況編譯器都保證了Block是在堆上創建的,如下代碼所示,僅最后一行代碼直接使用一個不賦值給變量的Block,它的類才是__NSStackBlock

int count = 0;
    blk_t blk = ^(){
        NSLog(@"In Stack:%d", count);
    };

    NSLog(@"blk's Class:%@", [blk class]);//打印:blk's Class:__NSMallocBlock__
    NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__
    NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__
    NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打印:Stack Block:__NSStackBlock__

另外,原書存在ARC和MRC混合講解、區分不明的情況,比如書中幾個使用到棧上對象導致Crash的例子是MRC條件下才會發生的,但書中沒做特殊說明。

使用__block發生了什么

Block捕獲的自動變量添加__block說明符,就可在Block內讀和寫該變量,也可以在原來的棧上讀寫該變量。

自動變量的截獲保證了棧上的自動變量被銷毀后,Block內仍可使用該變量。

__block保證了棧上和Block內(通常在堆上)可以訪問和修改 “同一個變量” ,__block是如何實現這一功能的?

__block發揮作用的 原理 :將棧上用__block修飾的自動變量 封裝成一個結構體 ,讓其在堆上創建,以方便從棧上或堆上訪問和修改同一份數據。

驗證過程:

現在對剛才的代碼段,加上__block說明符,并在block內外讀寫變量count。

int main() {
    __block int count = 10;
    void (^ blk)() = ^(){
        count = 20;
        NSLog(@"In Block:%d", count);//打印:In Block:20
    };
    count ++;
    NSLog(@"Out Block:%d", count);//打印:Out Block:11

    blk();
}

將上面的代碼段clang,發現Block的結構體 __main_block_impl_0 結構如下所示:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_count_0 *count; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

最大的變化就是count變量不再是int類型了,count變成了一個指向 __Block_byref_count_0 結構體的指針, __Block_byref_count_0 結構如下:

struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding;
 int __flags;
 int __size;
 int count;
};

它保存了int count變量,還有一個指向 __Block_byref_count_0 實例的指針 __forwarding ,通過下面兩段代碼 __forwarding 指針的用法可以知道,該指針其實指向的是對象自身:

//Block的執行函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_count_0 *count = __cself->count; // bound by ref

        (count->__forwarding->count) = 20;//對應count = 20;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_0, 
        (count->__forwarding->count));
    }
//main函數
int main() {
    __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,
    (__Block_byref_count_0 *)&count, 
    0, 
    sizeof(__Block_byref_count_0), 
    10};

    void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, 
    &__main_block_desc_0_DATA, 
    (__Block_byref_count_0 *)&count, 
    570425344));

    (count.__forwarding->count) ++;//對應count ++;

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_1, 
    (count.__forwarding->count));

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

為什么要通過__forwarding指針完成對count變量的讀寫修改?

為了保證無論是在棧上還是在堆上,都能通過都__forwarding指針找到在堆上創建的count這個 main_block_func_0結構體,以完成對count->count(第一個count是 main_block_func_0對象,第二個count是int類型變量)的訪問和修改。

示意圖如下:

 

來自:http://www.jianshu.com/p/d28a5633b963

 

 本文由用戶 dyydp 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!