在Objective-C中,block是一個很常見的東西,說白了就是個匿名函數,網上有不少關於block如何使用的文章,講的都很是精彩,這裏主要探討下block的實現原理。關於如何使用block,請參考網上的教程。c++
先來新建一個控制檯工程,main.m裏的代碼以下,並思考下最後的輸出結果是什麼:函數
void blockFunc1() { int num = 100; void (^block)(void) = ^() { NSLog(@"num = %d\n", num); }; num = 200; block(); } void blockFunc2() { __block int num = 100; void (^block)(void) = ^() { NSLog(@"num = %d\n", num); }; num = 200; block(); } int num = 100; void blockFunc3() { void (^block)(void) = ^() { NSLog(@"num = %d\n", num); }; num = 200; block(); } void blockFunc4() { static int num = 100; void (^block)(void) = ^{ NSLog(@"num = %d", num); }; num = 200; block(); } int main(int argc, const char * argv[]) { @autoreleasepool { ^{ printf("Hello, World!\n"); } (); blockFunc1(); blockFunc2(); blockFunc3(); blockFunc4(); } return 0; }
打印結果是:源碼分析
Hello, World! 2018-03-21 15:50:01.996591+0800 BlockDemo[34825:4848536] num = 100 2018-03-21 15:50:01.997009+0800 BlockDemo[34825:4848536] num = 200 2018-03-21 15:50:01.997025+0800 BlockDemo[34825:4848536] num = 200 2018-03-21 15:50:01.997037+0800 BlockDemo[34825:4848536] num = 200
聰明的讀者應該早就知道這個結果,?,先簡單解釋一下:
一、^{ printf("Hello, World!\n"); } ();
這個沒啥好說的,確定就打印「Hello, World!」了。指針
二、blockFunc1
裏面,num
是以值傳遞的方式被block獲取,因此儘管後面更改了num
的值,可是在block裏面仍是保持保持原來的值。code
二、blockFunc2
裏面,num由__block
修飾,num
在block變成了外部的一個引用(後面會經過源碼解釋),因此在block外部改變num
的值時,block裏面的num
也隨着改變。對象
三、blockFunc3
裏面,block引用的是一個全局的num,因此,num改變的時候也會改變block內部num的值。教程
四、blockFunc3
裏面,block引用的是一個static的num,因此,num改變也會改變block內部的num的值。get
也許你們看到上面的解釋仍是不知道爲啥會這樣,因此接下,我經過源碼來分析下其中的原因,咱們先把這段先轉換成c++文件,cd到main.m所在的目錄,並執行這條命令clang -rewrite-objc main.m
,經過這條命令能夠把main.m文件轉換成cpp文件,裏面能夠看到block的結構。咱們打開這份文件,這個文件比較長,直接拉到最後。能夠看到在文件的最後是main函數的入口,代碼以下:源碼
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)) (); blockFunc1(); blockFunc2(); blockFunc3(); blockFunc4(); } return 0; }
先看第一行代碼,構造了一個__main_block_impl_0對象,__main_block_impl_0是一個結構體。相關代碼以下:it
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; } };
從代碼可知,最後block會轉化成一個__block_impl對象,而block執行的代碼會轉化成一個靜態函數,__block_impl裏面的FuncPtr會指向這個靜態函數。在這裏printf("Hello, World!\n");
這個block轉換後的靜態函數以下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Hello, World!\n"); }
因此整個過程是這樣的:
一、先構造一個__main_block_impl_0
對象,構造的時候把__main_block_func_0
傳進去,固然還有別的參數,這裏先不考慮。
二、在__main_block_impl_0
的構造方法中,再把__main_block_func_0
賦給__block_impl
的FuncPtr。
三、調用FuncPtr。
因此,從上面能夠看出,block其實是轉化爲了一個__block_impl
對象,這個對象有isa指針,用來表示block的類型,上面的block的isa指向&_NSConcreteStackBlock。同時block對象還有一個FuncPtr指針,用來指向block執行的方法(轉換後的靜態函數)。
再來看看blockFunc1相關的內容
struct __blockFunc1_block_impl_0 { struct __block_impl impl; struct __blockFunc1_block_desc_0* Desc; int num; __blockFunc1_block_impl_0(void *fp, struct __blockFunc1_block_desc_0 *desc, int _num, int flags=0) : num(_num) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __blockFunc1_block_func_0(struct __blockFunc1_block_impl_0 *__cself) { int num = __cself->num; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_0, num); } void blockFunc1() { int num = 100; void (*block)(void) = ((void (*)())&__blockFunc1_block_impl_0((void *)__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num)); num = 200; ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); }
在這個函數裏面,構造block的時候把num傳了進去,並且是普通值傳遞,這樣的話實際上是拷貝了一份num。而後在執行block方法的時候,使用的是拷貝的那份num,從int num = __cself->num; // bound by copy
能夠看出。這個block也是_NSConcreteStackBlock類型的。
再來看看__block修飾過的num在block裏面是怎麼傳遞的,咱們看看blockFunc2相關的代碼:
// 封裝num的結構 struct __Block_byref_num_0 { void *__isa; __Block_byref_num_0 *__forwarding; int __flags; int __size; int num; }; struct __blockFunc2_block_impl_0 { struct __block_impl impl; struct __blockFunc2_block_desc_0* Desc; __Block_byref_num_0 *num; // by ref __blockFunc2_block_impl_0(void *fp, struct __blockFunc2_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __blockFunc2_block_func_0(struct __blockFunc2_block_impl_0 *__cself) { __Block_byref_num_0 *num = __cself->num; // bound by ref NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_1, (num->__forwarding->num)); } void blockFunc2() { __attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 100}; void (*block)(void) = ((void (*)())&__blockFunc2_block_impl_0((void *)__blockFunc2_block_func_0, &__blockFunc2_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344)); (num.__forwarding->num) = 200; ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); }
從代碼中能夠看到,__block修飾的num在內部被包裝成一個__Block_byref_num_0的對象,假設叫a,原來num的值100存儲在對象a的num字段中,同時這個對象a有一個__forwarding字段,指向a自己。當改變num的值的時候(源代碼是num = 200;
),這段代碼變爲(num.__forwarding->num) = 200;
,也就是說把對象a裏面的num字段的值變爲了200。同時,在block的執行函數__blockFunc2_block_func_0中,打印出來的取值是從__Block_byref_num_0 *num = __cself->num;
取出,也就是取得是改變後的值,因此打印結果是200。這就是爲何用__block修飾的變量能夠在block內部被修改。
那當num爲全局變量的時候,block又是怎樣的呢?請看代碼:
static void __blockFunc3_block_func_0(struct __blockFunc3_block_impl_0 *__cself) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_2, num); } void blockFunc3() { void (*block)(void) = ((void (*)())&__blockFunc3_block_impl_0((void *)__blockFunc3_block_func_0, &__blockFunc3_block_desc_0_DATA)); num = 200; ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); }
從代碼裏能夠看出,這種狀況很簡單,block對象根本沒有num字段,也就是打印的時候直接取得全局的num。
最後一種狀況也很簡單,當num時static的時候,構造block對象的時候直接用引用傳值的方式把num放到block對象中。因此,當外部改變num的值的時候,也能反映到block內部。代碼以下:
void blockFunc4() { static int num = 100; void (*block)(void) = ((void (*)())&__blockFunc4_block_impl_0((void *)__blockFunc4_block_func_0, &__blockFunc4_block_desc_0_DATA, &num)); num = 200; ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); }
這篇文章主要從源碼的角度講述了block的實現機制,並針對四種狀況分析了block是如何引用外部變量的,分別是:
一、當引用局部變量的時候,若是沒有__block修飾,那麼在block內部獲取的是外部變量的一份拷貝,改變外部變量不影響block內部的那份拷貝。
二、當引用局部變量的時候,同時局部變量用__block修飾,那麼在block內部使用的其實是外部變量的一個引用,因此改變外部變量會影響block內部變量的值。
三、當引用全局變量的時候,block並不持有這個變量。
四、當引用static變量的時候,block會以引用的方式持有這個變量。當在外部修改這個變量的時候,會影響block內部持有的這個變量的值。