在Objective-C中,block是一个很常见的东西,说白了就是个匿名函数,网上有很多关于block如何使用的文章,讲的都非常精彩,这里主要探讨下block的实现原理。
先看一个例子 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 45 46 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; }
打印结果:
1 2 3 4 5 Hello, World! 2021 -05 -19 11 :25 :19.532572 +0800 OCDemo[60496 :2272335 ] num = 100 2021 -05 -19 11 :25 :19.534041 +0800 OCDemo[60496 :2272335 ] num = 200 2021 -05 -19 11 :25 :19.534166 +0800 OCDemo[60496 :2272335 ] num = 200 2021 -05 -19 11 :25 :19.534292 +0800 OCDemo[60496 :2272335 ] num = 200
我们先简单解释一下:
1、”Hello, World!”应该很简单,执行block直接打印。
2、blockFunc1
里面,num
是以值传递的方式被block获取,所以尽管后面更改了num
的值,但是在block里面还是保持保持原来的值。
3、blockFunc2
里面,num由__block
修饰,num
在block变成了外部的一个引用(后面会通过源码解释),所以在block外部改变num
的值时,block里面的num
也随着改变。
4、blockFunc3
里面,block引用的是一个全局的num,所以,num改变的时候也会改变block内部num的值。
5、blockFunc3
里面,block引用的是一个static的num,所以,num改变也会改变block内部的num的值。
源码分析 也许大家看到上面的解释还是不知道为啥会这样,所以接下,我通过源码来分析下其中的缘由,我们先把这段先转换成c++文件,cd到main.m所在的目录,并执行这条命令clang -rewrite-objc main.m
,
如果上面的命令报错'UIKit/UIKit.h' file not found
,可以使用下面命令
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
通过这条命令可以把main.m文件转换成cpp文件,里面可以看到block的结构。我们打开这份文件,这个文件比较长,直接拉到最后。可以看到在文件的最后是main函数的入口,代码如下:
1 2 3 4 5 6 7 8 9 10 11 int main (int argc, const char * argv[]) { { __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是一个结构体。相关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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转换后的静态函数如下:
1 2 3 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf ("Hello, World!\n" ); }
所以整个过程是这样的: 1、先构造一个__main_block_impl_0
对象,构造的时候把__main_block_func_0
传进去,当然还有别的参数,这里先不考虑。
2、在__main_block_impl_0
的构造方法中,再把__main_block_func_0
赋给__block_impl
的FuncPtr。
3、调用FuncPtr。
所以,从上面可以看出,block实际上是转化为了一个__block_impl
对象,这个对象有isa指针,用来表示block的类型,上面的block的isa指向&_NSConcreteStackBlock。同时block对象还有一个FuncPtr指针,用来指向block执行的方法(转换后的静态函数)。
再来看看blockFunc1相关的内容
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 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; NSLog((NSString *)&__NSConstantStringImpl__var_folders__s_l_p47fxn36j8d2_n75spyb7c0000gn_T_main_324f63_mi_0, num); } static struct __blockFunc1_block_desc_0 { size_t reserved; size_t Block_size; } __blockFunc1_block_desc_0_DATA = { 0 , sizeof (struct __blockFunc1_block_impl_0)}; 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相关的代码:
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 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; __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; NSLog((NSString *)&__NSConstantStringImpl__var_folders__s_l_p47fxn36j8d2_n75spyb7c0000gn_T_main_324f63_mi_1, (num->__forwarding->num)); } static void __blockFunc2_block_copy_0(struct __blockFunc2_block_impl_0*dst, struct __blockFunc2_block_impl_0*src) {_Block_object_assign((void *)&dst->num, (void *)src->num, 8 );}static void __blockFunc2_block_dispose_0(struct __blockFunc2_block_impl_0*src) {_Block_object_dispose((void *)src->num, 8 );}static struct __blockFunc2_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __blockFunc2_block_impl_0*, struct __blockFunc2_block_impl_0*); void (*dispose)(struct __blockFunc2_block_impl_0*); } __blockFunc2_block_desc_0_DATA = { 0 , sizeof (struct __blockFunc2_block_impl_0), __blockFunc2_block_copy_0, __blockFunc2_block_dispose_0}; 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又是怎样的呢?请看代码:
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 int num = 100 ;struct __blockFunc3_block_impl_0 { struct __block_impl impl ; struct __blockFunc3_block_desc_0 * Desc ; __blockFunc3_block_impl_0(void *fp, struct __blockFunc3_block_desc_0 *desc, int flags=0 ) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __blockFunc3_block_func_0(struct __blockFunc3_block_impl_0 *__cself) { NSLog((NSString *)&__NSConstantStringImpl__var_folders__s_l_p47fxn36j8d2_n75spyb7c0000gn_T_main_324f63_mi_2, num); } static struct __blockFunc3_block_desc_0 { size_t reserved; size_t Block_size; } __blockFunc3_block_desc_0_DATA = { 0 , sizeof (struct __blockFunc3_block_impl_0)}; 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内部。代码如下:
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 struct __blockFunc4_block_impl_0 { struct __block_impl impl ; struct __blockFunc4_block_desc_0 * Desc ; int *num; __blockFunc4_block_impl_0(void *fp, struct __blockFunc4_block_desc_0 *desc, int *_num, int flags=0 ) : num(_num) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __blockFunc4_block_func_0(struct __blockFunc4_block_impl_0 *__cself) { int *num = __cself->num; NSLog((NSString *)&__NSConstantStringImpl__var_folders__s_l_p47fxn36j8d2_n75spyb7c0000gn_T_main_324f63_mi_3, (*num)); } static struct __blockFunc4_block_desc_0 { size_t reserved; size_t Block_size; } __blockFunc4_block_desc_0_DATA = { 0 , sizeof (struct __blockFunc4_block_impl_0)}; 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是如何引用外部变量的,分别是: 1、当引用局部变量的时候,如果没有__block修饰,那么在block内部获取的是外部变量的一份拷贝,改变外部变量不影响block内部的那份拷贝。
2、当引用局部变量的时候,同时局部变量用__block修饰,那么在block内部使用的实际上是外部变量的一个引用,所以改变外部变量会影响block内部变量的值。
3、当引用全局变量的时候,block并不持有这个变量。
4、当引用static变量的时候,block会以引用的方式持有这个变量。当在外部修改这个变量的时候,会影响block内部持有的这个变量的值。