來源:http://blog.devtang.com/blog/2013/07/28/a-look-inside-blocks/html
前言
這裏有關於block的5道測試題,建議你閱讀本文以前先作一下測試。ios
先介紹一下什麼是閉包。在wikipedia上,閉包的定義)是:objective-c
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的數據結構定義以下(圖片來自這裏):app
對應的結構體定義以下:異步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *); }; struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; /* Imported variables. */ }; |
經過該圖,咱們能夠知道,一個block實例實際上由6部分構成:ide
- isa指針,全部對象都有該指針,用於實現對象相關的功能。
- flags,用於按bit位表示一些block的附加信息,本文後面介紹block copy的實現代碼能夠看到對該變量的使用。
- reserved,保留變量。
- invoke,函數指針,指向具體的block實現的函數調用地址。
- descriptor, 表示該block的附加描述信息,主要是size大小,以及copy和dispose函數的指針。
- variables,capture過來的變量,block可以訪問它外部的局部變量,就是由於將這些變量(或變量的地址)複製到告終構體中。
該數據結構和後面的clang分析出來的結構實際是同樣的,不過僅是結構體的嵌套方式不同。但這一點我一開始沒有想明白,因此也給你們解釋一下,以下2個結構體SampleA和SampleB在內存上是徹底同樣的,緣由是結構體自己並不帶有任何額外的附加信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct SampleA { int a; int b; int c; }; struct SampleB { int a; struct Part1 { int b; }; struct Part2 { int c; }; }; |
在Objective-C語言中,一共有3種類型的block:
- _NSConcreteGlobalBlock 全局的靜態block,不會訪問任何外部變量。
- _NSConcreteStackBlock 保存在棧中的block,當函數返回時會被銷燬。
- _NSConcreteMallocBlock 保存在堆中的block,當引用計數爲0時會被銷燬。
咱們在下面會分別來查看它們各自的實現方式上的差異。
研究工具:clang
爲了研究編譯器是如何實現block的,咱們須要使用clang。clang提供一個命令,能夠將Objetive-C的源碼改寫成c語言的,藉此能夠研究block具體的源碼實現方式。該命令是
1
|
clang -rewrite-objc block.c |
NSConcreteGlobalBlock 類型的block的實現
咱們先新建一個名爲block1.c的源文件:
1 2 3 4 5 6 7 |
#include <stdio.h> int main() { ^{ printf("Hello, World!\n"); } (); return 0; } |
而後在命令行中輸入clang -rewrite-objc block1.c
便可在目錄中看到clang輸出了一個名爲block1.cpp的文件。該文件就是block在c語言實現,我將block1.cpp中一些無關的代碼去掉,將關鍵代碼引用以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Hello, World!\n"); } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) }; int main() { (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) (); return 0; } |
下面咱們就具體看一下是如何實現的。__main_block_impl_0就是該block的實現,從中咱們能夠看出:
- 一個block實際是一個對象,它主要由一個 isa 和 一個 impl 和 一個descriptor組成。
在本例中,isa指向 _NSConcreteGlobalBlock, 主要是爲了實現對象的全部特性,在此咱們就不展開討論了。- 因爲clang改寫的具體實現方式和LLVM不太同樣,而且這裏沒有開啓ARC。因此這裏咱們看到isa指向的仍是
_NSConcreteStackBlock
。但在LLVM的實現中,開啓ARC時,block應該是_NSConcreteGlobalBlock類型,具體能夠看《objective-c-blocks-quiz》第二題的解釋。 - impl是實際的函數指針,本例中,它指向__main_block_func_0。這裏的impl至關於以前提到的invoke變量,只是clang編譯器對變量的命名不同而已。
- descriptor是用於描述當前這個block的附加信息的,包括結構體的大小,須要capture和dispose的變量列表等。結構體大 小須要保存是由於,每一個block由於會capture一些變量,這些變量會加到__main_block_impl_0這個結構體中,使其體積變大。在 該例子中咱們還看不到相關capture的代碼,後面將會看到。
NSConcreteStackBlock 類型的block的實現
咱們另外新建一個名爲block2.c的文件,輸入如下內容:
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> int main() { int a = 100; void (^block2)(void) = ^{ printf("%d\n", a); }; block2(); return 0; } |
用以前提到的clang工具,轉換後的關鍵代碼以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int a; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int a = __cself->a; // bound by copy printf("%d\n", a); } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; int main() { int a = 100; void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a); ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2); return 0; } |
在本例中,咱們能夠看到:
- 本例中,isa指向_NSConcreteStackBlock,說明這是一個分配在棧上的實例。
- main_block_impl_0 中增長了一個變量a,在block中引用的變量a實際是在申明block時,被複制到main_block_impl_0結構體中的那個變量a。由於這樣,咱們就能理解,在block內部修改變量a的內容,不會影響外部的實際變量a。
- main_block_impl_0 中因爲增長了一個變量a,因此結構體的大小變大了,該結構體大小被寫在了main_block_desc_0中。
咱們修改上面的源碼,在變量前面增長__block關鍵字:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> int main() { __block int i = 1024; void (^block1)(void) = ^{ printf("%d\n", i); i = 1023; }; block1(); return 0; } |
生成的關鍵代碼以下,能夠看到,差別至關大:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
struct __Block_byref_i_0 { void *__isa; __Block_byref_i_0 *__forwarding; int __flags; int __size; int i; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_i_0 *i; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_i_0 *i = __cself->i; // bound by ref printf("%d\n", (i->__forwarding->i)); (i->__forwarding->i) = 1023; } static void __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*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);} static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; int main() { __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024}; void (*block1)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344); ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1); return 0; } |
從代碼中咱們能夠看到:
- 源碼中增長一個名爲__Block_byref_i_0 的結構體,用來保存咱們要capture而且修改的變量i。
- main_block_impl_0 中引用的是Block_byref_i_0的結構體指針,這樣就能夠達到修改外部變量的做用。
- __Block_byref_i_0結構體中帶有isa,說明它也是一個對象。
- 咱們須要負責Block_byref_i_0結構體相關的內存管理,因此main_block_desc_0中增長了copy和dispose函數指針,對於在調用先後修改相應變量的引用計數。
NSConcreteMallocBlock 類型的block的實現
NSConcreteMallocBlock類型的block一般不會在源碼中直接出現,由於默認它是當一個block被copy的時候,纔會將這個block複製到堆中。如下是一個block被copy時的示例代碼(來自這裏),能夠看到,在第8步,目標的block類型被修改成_NSConcreteMallocBlock。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
static void *_Block_copy_internal(const void *arg, const int flags) { struct Block_layout *aBlock; const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE; // 1 if (!arg) return NULL; // 2 aBlock = (struct Block_layout *)arg; // 3 if (aBlock->flags & BLOCK_NEEDS_FREE) { // latches on high latching_incr_int(&aBlock->flags); return aBlock; } // 4 else if (aBlock->flags & BLOCK_IS_GLOBAL) { return aBlock; } // 5 struct Block_layout *result = malloc(aBlock->descriptor->size); if (!result) return (void *)0; // 6 memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first // 7 result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed result->flags |= BLOCK_NEEDS_FREE | 1; // 8 result->isa = _NSConcreteMallocBlock; // 9 if (result->flags & BLOCK_HAS_COPY_DISPOSE) { (*aBlock->descriptor->copy)(result, aBlock); // do fixup } return result; } |
變量的複製
對於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 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { int i = 1024; void (^block1)(void) = ^{ printf("%d\n", i); }; block1(); NSLog(@"%@", block1); } return 0; } |
我我的認爲這麼作的緣由是,因爲ARC已經能很好地處理對象的生命週期的管理,這樣全部對象都放到堆上管理,對於編譯器實現來講,會比較方便。
參考連接
但願本文能加深你對於block的理解。我在學習中,查閱瞭如下文章,一併分享給你們。祝你們玩得開心~