首发地址:block底层实现与变量捕获
本文已经添加到专辑:《彻底弄懂OC》。 欢迎加入我的QQ群:661461410
,一起探讨iOS底层原理。
block又叫代码块,是OC语法中非常重要的一个概念,我们先来看一下Block的简单使用。
int main(int argc, const char * argv[]) { @autoreleasepool { ^{ NSLog(@"hello block"); }(); int d = 5; void (^block)(int, int) = ^(int a, int b) { int c = a + b + d; NSLog(@"a + b + d = %d", c); }; block(3, 4); } return 0; } 复制代码
上面的代码中,我们创建了两个Block,一个直接执行,输出Hello World
。 一个通过block变量进行调用,并引用了一个外部变量d。输出12
。
我们将以上代码编译成C代码:
# 在main.m所在目录执行该命令。 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 复制代码
从main-arm64.cpp文件中,我们可以看到Block的结构如下:
struct __main_block_impl_1 { struct __block_impl impl; struct __main_block_desc_1* Desc; int d; __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _d, int flags=0) : d(_d) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; 复制代码
我们可以看出Block的底层是结构体,__main_block_impl_1
包含一个变量impl
其结构和 Class
的结构类似,其包含一个isa
指针,可见Block本质上也是一个类,其中FuncPtr
表示要执行的代码块的函数地址。d
表示它引用的外部变量。
下面,我们一起看一下Block的调用过程,首先我们将下面代码,编译成C代码。
int main(int argc, const char * argv[]) { @autoreleasepool { void (^block)(void) = ^{ NSLog(@"hello block"); }; block(); } return 0; } // 下面是编译后的C代码 int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; //可以看得出来,Block的调用集中在这两行。 void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); } return 0; } static void __main_block_func_0(struct __main_block_impl_0 *__cself) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_bv_k_7y193n6tvf34wjvqvnn3q40000gn_T_main_0422f2_mi_0); } 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)}; 复制代码
针对,上面的两行代码,先调用__main_block_impl_0
的结构体构造函数,创建Block,并将地址赋值给我block。而__main_block_func_0
是对应block内部要执行的代码,是一个静态的方法,它会赋值给__block_impl
中的FuncPtr
。
__main_block_desc_0_DATA
是也是一个结构体变量,里面的两个参数reserved
为 0, Block_size
为 __main_block_impl_0
结构体的大小。
调用的时候,是从block里面直接取出FuncPtr
。 我们知道block
是__main_block_impl_0
类型,由于结构体的特性,将block
强转为__block_impl
类型,是可以直接取到FuncPtr
的。所以第二句的调用也是清晰的。
上面的两句代码去掉强制类型转化,可以精简为:
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)); block->FuncPtr(block); 复制代码
这样,调用过程就清晰多了。
通过上面的分析,我们可以看出block的结构应该是如下图所示:
auto自动变量是离开作用域,就会销毁, 只存在局部变量里面,不能修饰全局变量。
比如,下面例子中的age
和 weight
就是auto变量,他们离开自己所在的作用局就会销毁。默认情况下auto关键字会自动添加。
int main(int argc, const char * argv[]) { @autoreleasepool { { int age = 20; auto int weight = 60; } // 在这里访问age, weight就报错了。 } return 0; } 复制代码
如果block中使用了auto变量,那么block就会捕获该变量,下面代码
int main(int argc, const char * argv[]) { @autoreleasepool { { int age = 20; auto int weight = 60; void (^block)(void) = ^{ NSLog(@"age = %d, weight = %d", age, weight); //age的结果是20 }; age = 40; block(); } } return 0; } 复制代码
打印的结果中 age为20 还是 40? 编译后,__main_block_impl_0
的结构如下,增加了两个int 变量。
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; int weight; //: age(_age), weight(_weight) 是C++语法,表示参数_age会赋值给变量age. __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int _weight, int flags=0) : age(_age), weight(_weight) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; 复制代码
我们可以看出,捕获了auto变量,而且是值传递。
下面代码输入结果是什么?
int main(int argc, const char * argv[]) { @autoreleasepool { { static int height = 40; void (^block)(void) = ^{ NSLog(@"height = %d, ", height); }; height = 80; block(); } } return 0; } 复制代码
结果是80,为什么呢? 我们依然通过编译后的结果查看。
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int *height; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_height, int flags=0) : height(_height) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; { static int height = 40; void (*block)(void) = ((void (*)())&__ma.in_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &height)); height = 80; ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); } } return 0; } static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int *height = __cself->height; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_bv_k_7y193n6tvf34wjvqvnn3q40000gn_T_main_8d6bbf_mi_0, (*height)); } 复制代码
我们可以看出__main_block_impl_0
中增加了一个变量height
,但需要注意的是它是int *
类型的,在给它赋值的时候传入的是&height
。 在__main_block_func_0
中访问的时候是通过*height
取值的。
因此我们可以得出结论,静态变量也是会被Block捕获的,但它捕获的是指针。
下面代码,输出的结果是什么?
int age = 10; int main(int argc, const char * argv[]) { @autoreleasepool { { void (^block)(void) = ^{ NSLog(@"height = %d, ", age); }; age = 20; block(); } } return 0; } 复制代码
输入结果是20,那block捕获了age吗?是通过指针访问的吗?我们看一下编译结果:
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并没有捕获全局变量。
通过上面的分析,我们可以得出结论:
下面代码中,Person类中的test方法中block捕获了变量了吗?捕获了那个变量?
// main.m #import <Foundation/Foundation.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; p.name = @"乐戈"; [p test]; } return 0; } // Person.h @interface Person : NSObject @property (nonatomic, copy)NSString *name; - (void)test; @end // Person.m @implementation Person - (void)test { void (^block)(void) = ^{ NSLog(@"name == %@", self.name); }; block(); } @end 复制代码
用上面的命令将Person.m编译C++代码,如下:
struct __Person__test_block_impl_0 { struct __block_impl impl; struct __Person__test_block_desc_0* Desc; Person *self; __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; 复制代码
可以看出,这里捕获的并不是name,而是Person对象,这涉及了block的循环引用,我们将在下面的文章中讲述。
下面各个代码的输出结果是什么?
//问题1 int main(int argc, const char * argv[]) { @autoreleasepool { NSMutableArray *array = [@[@"abc"] mutableCopy]; void (^block)(void) = ^{ NSLog(@"hello block---%@", [array firstObject]); }; array[0] = @"dgf"; block(); } return 0; } //问题2 int main(int argc, const char * argv[]) { @autoreleasepool { NSMutableArray *array = [@[@"abc"] mutableCopy]; void (^block)(void) = ^{ NSLog(@"hello block---%@", [array firstObject]); }; array = [@[@"dgf"] mutableCopy]; block(); } return 0; } 复制代码