談Objective-C Block的實現

轉自唐巧的博客

前言

這裏有關於block的5道測試題,建議你閱讀本文之前先做一下測試。
先介紹一下什麼是閉包。在wikipedia上,閉包的定義)是:
In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
翻譯過來,閉包是一個函數(或指向函數的指針),再加上該函數執行的外部的上下文變量(有時候也稱作自由變量)。
block實際上就是Objective-C語言對於閉包的實現。 block配合上dispatch_queue,可以方便地實現簡單的多線程編程和異步編程,關於這個,我之前寫過一篇文章介紹:《使用GCD》
本文主要介紹Objective-C語言的block在編譯器中的實現方式。主要包括:
block的內部實現數據結構介紹
block的三種類型及其相關的內存管理方式
block如何通過capture變量來達到訪問函數外的變量
實現方式
數據結構定義
block的數據結構定義如下(圖片來自這裏):
對應的結構體定義如下:
  1. struct Block_descriptor {

  2.    unsigned longint reserved;

  3.    unsigned longint size;

  4. void (*copy)(void *dst, void *src);

  5. void (*dispose)(void *);

  6. };


  7. struct Block_layout {

  8. void *isa;

  9. int flags;

  10. int reserved;

  11. void (*invoke)(void *, ...);

  12.    struct Block_descriptor *descriptor;

  13. /* Imported variables. */

  14. };

通過該圖,我們可以知道,一個block實例實際上由6部分構成:
1.isa指針,所有對象都有該指針,用於實現對象相關的功能。
2.flags,用於按bit位表示一些block的附加信息,本文後面介紹block copy的實現代碼可以看到對該變量的使用。
3.reserved,保留變量。
4.invoke,函數指針,指向具體的block實現的函數調用地址。
5.descriptor, 表示該block的附加描述信息,主要是size大小,以及copy和dispose函數的指針。
6.variables,capture過來的變量,block能夠訪問它外部的局部變量,就是因爲將這些變量(或變量的地址)複製到了結構體中。
該數據結構和後面的clang分析出來的結構實際是一樣的,不過僅是結構體的嵌套方式不一樣。但這一點我一開始沒有想明白,所以也給大家解釋一下,如下2個結構體SampleA和SampleB在內存上是完全一樣的,原因是結構體本身並不帶有任何額外的附加信息。
  1. struct SampleA {

  2. int a;

  3. int b;

  4. int c;

  5. };


  6. struct SampleB {

  7. int a;

  8.    struct Part1 {

  9. int b;

  10.    };

  11.    struct Part2 {

  12. int c;

  13.    };

  14. };

在Objective-C語言中,一共有3種類型的block:
1._NSConcreteGlobalBlock 全局的靜態block,不會訪問任何外部變量。
2._NSConcreteStackBlock 保存在棧中的block,當函數返回時會被銷燬。
3._NSConcreteMallocBlock 保存在堆中的block,當引用計數爲0時會被銷燬。
我們在下面會分別來查看它們各自的實現方式上的差別。
研究工具:clang
爲了研究編譯器是如何實現block的,我們需要使用clang。clang提供一個命令,可以將Objetive-C的源碼改寫成c語言的,藉此可以研究block具體的源碼實現方式。該命令是
  1. clang -rewrite-objc block.c

NSConcreteGlobalBlock 類型的block的實現
我們先新建一個名爲block1.c的源文件:
  1. #include <stdio.h>


  2. int main()

  3. {

  4.    ^{ printf("Hello, World!\n"); } ();

  5. return 0;

  6. }

然後在命令行中輸入clang -rewrite-objc block1.c即可在目錄中看到clang輸出了一個名爲block1.cpp的文件。該文件就是block在c語言實現,我將block1.cpp中一些無關的代碼去掉,將關鍵代碼引用如下:
  1. struct __block_impl {

  2. void *isa;

  3. int Flags;

  4. int Reserved;

  5. void *FuncPtr;

  6. };


  7. struct __main_block_impl_0 {

  8.    struct __block_impl impl;

  9.    struct __main_block_desc_0* Desc;

  10.    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {

  11.        impl.isa = &_NSConcreteStackBlock;

  12.        impl.Flags = flags;

  13.        impl.FuncPtr = fp;

  14.        Desc = desc;

  15.    }

  16. };

  17. staticvoid __main_block_func_0(struct __main_block_impl_0 *__cself) {

  18.    printf("Hello, World!\n");

  19. }


  20. static struct __main_block_desc_0 {

  21.    size_t reserved;

  22.    size_t Block_size;

  23. } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) };


  24. int main()

  25. {

  26.    (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();

  27. return 0;

  28. }

下面我們就具體看一下是如何實現的。__main_block_impl_0就是該block的實現,從中我們可以看出:
1.一個block實際是一個對象,它主要由一個 isa 和 一個 impl 和 一個descriptor組成。
2.在本例中,isa指向 _NSConcreteGlobalBlock, 主要是爲了實現對象的所有特性,在此我們就不展開討論了。
3.impl是實際的函數指針,本例中,它指向__main_block_func_0。這裏的impl相當於之前提到的invoke變量,只是clang編譯器對變量的命名不一樣而已。
4.descriptor是用於描述當前這個block的附加信息的,包括結構體的大小,需要capture和dispose的變量列表等。結構體大小需要保存是因爲,每個block因爲會capture一些變量,這些變量會加到__main_block_impl_0這個結構體中,使其體積變大。在該例子中我們還看不到相關capture的代碼,後面將會看到。
NSConcreteStackBlock 類型的block的實現
我們另外新建一個名爲block2.c的文件,輸入以下內容:
  1. #include <stdio.h>


  2. int main() {

  3. int a = 100;

  4. void (^block2)(void) = ^{

  5.        printf("%d\n", a);

  6.    };

  7.    block2();


  8. return 0;

  9. }

用之前提到的clang工具,轉換後的關鍵代碼如下:
  1. struct __main_block_impl_0 {

  2.    struct __block_impl impl;

  3.    struct __main_block_desc_0* Desc;

  4. int a;

  5.    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {

  6.        impl.isa = &_NSConcreteStackBlock;

  7.        impl.Flags = flags;

  8.        impl.FuncPtr = fp;

  9.        Desc = desc;

  10.    }

  11. };

  12. staticvoid __main_block_func_0(struct __main_block_impl_0 *__cself) {

  13. int a = __cself->a; // bound by copy

  14.    printf("%d\n", a);

  15. }


  16. static struct __main_block_desc_0 {

  17.    size_t reserved;

  18.    size_t Block_size;

  19. } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};


  20. int main()

  21. {

  22. int a = 100;

  23. void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);

  24.    ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);


  25. return 0;

  26. }

在本例中,我們可以看到:
1.本例中,isa指向_NSConcreteStackBlock,說明這是一個分配在棧上的實例。
2.main_block_impl_0 中增加了一個變量a,在block中引用的變量a實際是在申明block時,被複制到main_block_impl_0結構體中的那個變量a。因爲這樣,我們就能理解,在block內部修改變量a的內容,不會影響外部的實際變量a。
3.main_block_impl_0 中由於增加了一個變量a,所以結構體的大小變大了,該結構體大小被寫在了main_block_desc_0中。
我們修改上面的源碼,在變量前面增加__block關鍵字:
  1. #include <stdio.h>


  2. int main()

  3. {

  4.    __block int i = 1024;

  5. void (^block1)(void) = ^{

  6.        printf("%d\n", i);

  7.        i = 1023;

  8.    };

  9.    block1();

  10. return 0;

  11. }

生成的關鍵代碼如下,可以看到,差異相當大:
  1. struct __Block_byref_i_0 {

  2. void *__isa;

  3.    __Block_byref_i_0 *__forwarding;

  4. int __flags;

  5. int __size;

  6. int i;

  7. };


  8. struct __main_block_impl_0 {

  9.    struct __block_impl impl;

  10.    struct __main_block_desc_0* Desc;

  11.    __Block_byref_i_0 *i; // by ref

  12.    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {

  13.        impl.isa = &_NSConcreteStackBlock;

  14.        impl.Flags = flags;

  15.        impl.FuncPtr = fp;

  16.        Desc = desc;

  17.    }

  18. };

  19. staticvoid __main_block_func_0(struct __main_block_impl_0 *__cself) {

  20.    __Block_byref_i_0 *i = __cself->i; // bound by ref


  21.    printf("%d\n", (i->__forwarding->i));

  22.    (i->__forwarding->i) = 1023;

  23. }


  24. staticvoid __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}


  25. staticvoid __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}


  26. static struct __main_block_desc_0 {

  27.    size_t reserved;

  28.    size_t Block_size;

  29. void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);

  30. void (*dispose)(struct __main_block_impl_0*);

  31. } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};


  32. int main()

  33. {

  34.    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};

  35. void (*block1)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344);

  36.    ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);

  37. return 0;

  38. }

從代碼中我們可以看到:
1.源碼中增加一個名爲__Block_byref_i_0 的結構體,用來保存我們要capture並且修改的變量i。
2.main_block_impl_0 中引用的是Block_byref_i_0的結構體指針,這樣就可以達到修改外部變量的作用。
3.__Block_byref_i_0結構體中帶有isa,說明它也是一個對象。
4.我們需要負責Block_byref_i_0結構體相關的內存管理,所以main_block_desc_0中增加了copy和dispose函數指針,對於在調用前後修改相應變量的引用計數。
NSConcreteMallocBlock 類型的block的實現
NSConcreteMallocBlock類型的block通常不會在源碼中直接出現,因爲默認它是當一個block被copy的時候,纔會將這個block複製到堆中。以下是一個block被copy時的示例代碼(來自這裏),可以看到,在第8步,目標的block類型被修改爲_NSConcreteMallocBlock。
  1. staticvoid *_Block_copy_internal(constvoid *arg, constint flags) {

  2.    struct Block_layout *aBlock;

  3. const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;


  4. // 1

  5. if (!arg) return NULL;


  6. // 2

  7.    aBlock = (struct Block_layout *)arg;


  8. // 3

  9. if (aBlock->flags & BLOCK_NEEDS_FREE) {

  10. // latches on high

  11.        latching_incr_int(&aBlock->flags);

  12. return aBlock;

  13.    }


  14. // 4

  15. elseif (aBlock->flags & BLOCK_IS_GLOBAL) {

  16. return aBlock;

  17.    }


  18. // 5

  19.    struct Block_layout *result = malloc(aBlock->descriptor->size);

  20. if (!result) return (void *)0;


  21. // 6

  22.    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first


  23. // 7

  24.    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed

  25.    result->flags |= BLOCK_NEEDS_FREE | 1;


  26. // 8

  27.    result->isa = _NSConcreteMallocBlock;


  28. // 9

  29. if (result->flags & BLOCK_HAS_COPY_DISPOSE) {

  30.        (*aBlock->descriptor->copy)(result, aBlock); // do fixup

  31.    }


  32. return result;

  33. }

變量的複製
對於block外的變量引用,block默認是將其複製到其數據結構中來實現訪問的,如下圖所示(圖片來自這裏):
對於用__block修飾的外部變量引用,block是複製其引用地址來實現訪問的,如下圖所示(圖片來自這裏):
LLVM源碼
在LLVM開源的關於block的實現源碼,其內容也和我們用clang改寫得到的內容相似,印證了我們對於block內部數據結構的推測。
ARC對block類型的影響
在ARC開啓的情況下,將只會有 NSConcreteGlobalBlock和 NSConcreteMallocBlock類型的block。
原本的NSConcreteStackBlock的block會被NSConcreteMallocBlock類型的block替代。證明方式是以下代碼在XCode中,會輸出 <__NSMallocBlock__: 0x100109960>。在蘋果的官方文檔中也提到,當把棧中的block返回時,不需要調用copy方法了。
  1. #import <Foundation/Foundation.h>


  2. int main(int argc, constchar * argv[])

  3. {

  4.    @autoreleasepool {

  5. int i = 1024;

  6. void (^block1)(void) = ^{

  7.            printf("%d\n", i);

  8.        };

  9.        block1();

  10.        NSLog(@"%@", block1);

  11.    }

  12. return 0;

  13. }

我個人認爲這麼做的原因是,由於ARC已經能很好地處理對象的生命週期的管理,這樣所有對象都放到堆上管理,對於編譯器實現來說,會比較方便。
參考鏈接
希望本文能加深你對於block的理解。我在學習中,查閱了以下文章,一併分享給大家。祝大家玩得開心~


轉自:http://www.cocoachina.com/applenews/devnews/2013/0802/6725.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章