A block is an anonymous inline collection of code, and sometimes also called a “closure”。通俗的讲,block就是带有自动变量值的匿名函数。
Features
Block的实质
Block语法看起来很特别,但它实际上是作为普通的C语言源代码来处理的。但clang(LLVM编译器)居具有转换为我们可阅读的源代码的功能。通过clang -rewrite-objc + file's name
选项就能够将含有Block语法的源代码转换为C的源代码。
一段简单的源代码:
int main() { void (^blk)(void) = ^{ printf("Hello World!"); }; blk(); return 0; }
通过clang变换为以下形式:
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!"); } 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 (*blk)(void) = (void (*)())&__main_block_impl_0( (void *)__main_block_func_0, &__main_block_desc_0_DATA); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr) ((__block_impl *)blk); return 0; }
不想看那长长的一段代码的话直接看这幅结构图:
上面的源代码中并没有copy、dispose等,稍后解释
可以看出,一个block实际有6部分组成;
- isa指针,所有对象都有该指针,用于实现对象相关的功能。
- flags,用于按bit位表示一些block的附加信息,本文后面介绍block copy的实现代码可以看到对该变量的使用。
- reserved,保留变量。
- invoke,函数指针,指向具体的block实现的函数调用地址。
- descriptor, 表示该block的附加描述信息,主要是size大小,以及copy和 dispose函数的指针。
- variables,capture过来的变量,block能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。
对于该block的构造函数,如下代码,
void (*blk)(void) = (void (*)())&__main_block_impl_0( (void *)__main_block_func_0, &__main_block_desc_0_DATA);
去掉转化部分,具体为:
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA); struct __main_block_impl_0 *blk = &blk;
该源码将__main_block_impl_0结构体类型的自动变量赋值给__main_block_impl_0结构体指针类型的变量blk。
Block访问符,__block说明符
Block能够截获自动变量的值,但在实现上不能改写被截获的自动变量的值,因此当编译器在编译过程中检出给被截获的自动变量赋值的操作时,会产生编译错误。
解决这个问题有俩种方法。第一种:C语言中有一个变量,允许Block改写值。具体如下:
- 静态变量
- 静态全局变量
- 全局变量
虽然Block语法的匿名函数部分简单的变换为了C语言函数,但从这个变换的函数中访问静态全局变量/全局变量并没有任何改变,可直接使用。
但是静态变量的情况下,转换后的函数本来就设置在含有Block语法的函数外,所以无法从变量作用域访问。
查看下列源代码。
int global_val = 1; static int static_global_val = 3; int main(int argc, const char * argv[]) { @autoreleasepool { static int static_val = 5; void (^block)() = ^{ global_val *= 1; static_global_val *= 3; static_val *= 5; }; block(); printf("%d,%d,%d", global_val, static_global_val, static_val); } return 0; }
该源代码使用了Block改写全局变量global_val、静态全局变量static_global_val和静态变量static_val。通过clang转换之后如下:
int global_val = 1; static int static_global_val = 3; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int *static_val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0( struct __main_block_impl_0 *__cself) { int *static_val = __cself->static_val; // bound by copy global_val *= 1; static_global_val *= 3; (*static_val) *= 5; } 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 argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; static int static_val = 5; void (*block)() = (void (*)()) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val); ((void (*)(__block_impl *)) ((__block_impl *)block)->FuncPtr)((__block_impl *)block); printf("%d,%d,%d", global_val, static_global_val, static_val); } return 0; }
对于静态变量static_val的访问,查看方法__main_block_func_0,可以看出使用静态变量static_val的指针对其进行访问。将静态变量static_val的指针传递给__main_block_func_0结构体的构造函数并保存。这是超出作用域使用变量的最简单的方法。
解决Block不能保存值的第二种方法是使用”__block”说明符。更准确的表述方式为”__block存储域类说明符”。
在编译错误的变量前加上__block说明符
__block int val = 3; void (^block)() = ^{ val = 5; };
对该源代码进行编译,结果如下。
struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_val_0 *val; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref (val->__forwarding->val) = 5; } static void __main_block_copy_0 (struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } static void __main_block_dispose_0( struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->val, 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(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; __attribute__((__blocks__(byref))) __Block_byref_val_0 val = { (void*)0, (__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 3 }; void (*block)() = (void (*)())&__main_block_impl_0( (void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344); ((void (*)(__block_impl *)) ((__block_impl *)block)->FuncPtr)((__block_impl *)block); } return 0; }
__block变量var变成了结构体实例。__block变量也同Block一样变成__Block_byref_val_0结构体类型的自动变量,即栈上生成的__Block_byref_val_0结构体实例。这里可以看出上面留下的问题,cope和dispose是因为__block在clang之后出现的。
初始化block的时候,对__Block_byref_val_0结构体赋值,即为初始化的值。刚刚在Block中向静态变量赋值时,使用了该静态变量的指针。而向__block变量赋值要比这个复杂的多。Block的__main_block_impl_0结构体实例持有指向__block变量的__Block_byref_val_0结构体实例的指针。
__Block_byref_val_0结构体实例的成员变量__forwarding持有指向该实例自身的指针。通过成员变量__forwarding访问成员变量var。(成员变量var是该实例自身持有的变量,它相当于原自动变量。)如图所示。
另外,__block变量的__Block_byref_val_0结构体并不在Block的__main_block_impl_0结构体中,这样做是为了在多个Block中使用__block变量。我们看一下下面的源代码。
__block int var = 1; void (^blk0)() = ^{ var = 0; }; void (^blk1)() = ^{ var = 1; };
Block类型变量blk0和blk1访问__block变量var。将转换的之后的结果摘录出来。
struct __Block_byref_var_0 { void *__isa; __Block_byref_var_0 *__forwarding; int __flags; int __size; int var; }; void (*blk0)() = (void (*)())&__main_block_impl_0( (void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_var_0 *)&var, 570425344 ); void (*blk1)() = (void (*)())&__main_block_impl_1( (void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_var_0 *)&var, 570425344 );
俩个Block都是使用了__Block_byref_var_0结构体实例var的指针。这样一来就可以从多个Block中使用同一个__block变量。当然,反过来从一个Block中使用多个__block变量也是可以的。只要增加Block的结构体成员变量与构造函数的参数,便可对应多个使用多个__block变量。
Block存储域
通过前面的说明可知,Block转换为Block的结构体类型的自动变量,__block变量转换为__block变量的结构体类型的自动变量。所谓结构体类型的自动变量,即栈上生成的该结构体的实例。如下表所示。
名称 | 实质 |
---|---|
Block | 栈上Block的结构体实例 |
__block变量 | 栈上__block变量的结构体实例 |
之前的clang之后的代码中,出现了
impl.isa = &_NSConcreteStackBlock;
,说明该Block的类型为_NSConcreteStackBlock。同时与之相对应的类有:
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
对应的存储区域如下表。
类 | 对应的存储域 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 程序中的数据区域(.data区) |
_NSConcreteMallocBlock | 堆 |
应用程序的内存分配如下图。
到现为止出现的Block例子使用的都是_NSConcreteStackBlock类,且都设置在栈上。在记述全局变脸的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象。例如:
void (^blk)() = ^{ NSLog(@"Hello World!"); }; int main(int argc, const char * argv[]) { return 0; }
此源代码通过使用全局变量blk来使用Block语法。该Block的类在ARC和非ARC下都为_NSConcreteGlobalBlock。此Block即该Block的结构体实例设置在程序的数据区域中。由此Block的结构体实例的内容不依赖于执行时的状态,或者说为该Block不会capture变量,所以整个程序中只需要一个实例。因此将Block的结构体实例设置在与全局变量相同的数据区域中。
只在截获自动变量时,Block的结构体实例截获的值才会根据执行时的状态变化。例如一下源代码中,虽然多次使用同一个Block语法blk,但每个for循环中截获的自动变量的值都不同。
typedef int (^blk_t)(int); int main(int argc, const char * argv[]) { @autoreleasepool { for (int rate = 0; rate < 10; rate++) { blk_t blk = ^(int count) { return rate * count; }; blk_t blk2 = ^(int count) { return count; }; } } return 0; }
在非ARC下,blk为_NSConcreteStackBlock,blk2为_NSConcreteGlobalBlock
在ARC下,blk为_NSConcreteMallocBlock,blk2为_NSConcreteGlobalBlock
这里先给出ARC下的状态,稍后解释ARC下的不同,先重点关注非ARC。
配置在全局变量上的Block,从变量作用域外也可以通过指针安全的访问。但设置在栈上的Block,如果其所属的变量作用域结束,则该__block变量也会被废弃,如下图所示。
Block提供了将Block和__block变量从栈上复制到堆上的方法来解决这个问题。将配置在栈上的Block赋值到堆上,这样即使Block语法记述的变量作用域结束,堆上的Block还可以继续存在。如下图所示。
复制到堆上的Block将_NSConcreteMallocBlock类对象写入Block的结构体实例的成员变量isa。
impl.isa = &_NSConcreteMallocBlock;
而__block变量的结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是堆上都能正确访问__block变量。
有时在__block变量配置在堆上的状态下,也可以访问栈上的__block变量。在此情形下,只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上的__block变量还是从堆上的__block变量都能够正确访问。
那么Blocks提供的复制方法究竟是什么呢?实际上当ARC有效时,大多数情形下编译器会恰当的进行判断,自动生成将Block从栈上复制到堆上的代码。分析下面源代码。
typedef int (^blk_t)(int); blk_t func ( int rate ) { return ^(int count){ return rate * count; }; }
该源代码为返回配置在栈上的Block函数。即程序执行中从该函数返回函数调用方时变量作用域结束,因此栈上的Block也被废弃。
上面源代码是在ARC环境下(clang命令需要声明为ARC环境,clang -fobjc-arc -rewrite-objc file’s name),非ARC下会编译错误,提示Returning block that lives on the local stack。但是可以通过增加临时变量的方式使得编译通过。
typedef int (^blk_t)(int); blk_t func (int rate) { blk_t tmp = ^(int count) { return count * rate; }; return tmp; } int main(int argc, const char * argv[]) { return 0; }
把原来的返回值赋给一个变量,然后再返回这个变量。不过虽然编译通过了,这个返回的Block作用域仍是在函数栈中的,因此一旦函数运行完毕后再使用这个Block很可能会引发BAD_ACCESS错误。
下面我们来看下面的源代码。
id getBlockArray() { return @[^{NSLog(@"111");}, ^{NSLog(@"222");}]; } int main(int argc, const char * argv[]) { @autoreleasepool { NSArray *arr = getBlockArray(); NSLog(@"%@", arr); } return 0; }
该源代码为NSGlobalBlock,所以在ARC和非ARC下都能正常运行。
将该源代码稍微修改一下:
id getBlockArray() { int var = 1; return @[^{NSLog(@"blk0=%d", var);}, ^{NSLog(@"blk1=%d", var);}]; } int main(int argc, const char * argv[]) { @autoreleasepool { id obj = getBlockArray(); typedef void (^blk_t)(void); blk_t blk = (blk_t)[obj objectAtIndex:0]; blk(); } return 0; }
上面源代码在ARC下正常,为NSMallocBlock。
在非ARC下会在栈上生成2个Block,其中的blk(),在执行时发生异常,应用程序强制结束。这是由于在getBlockArray函数执行结束时,栈上的Block被废弃的缘故。此时编译器不能判断是否需要复制。也可以不让编译器进行判断,而使其在所有情况下都能复制。但将Block从栈上复制到堆上是相当消耗CPU的。当Block设置在栈上也能够使用时,将Block从栈上复制到堆上只是浪费资CPU资源。因此只能自己进行复制。
该源代码像下面这样修改一下即可正常运行。
id getBlockArray() { int var = 1; return @[[^{NSLog(@"blk0=%d", var);} copy], [^{NSLog(@"blk1=%d", var);} copy]]; } int main(int argc, const char * argv[]) { @autoreleasepool { id obj = getBlockArray(); typedef void (^blk_t)(void); blk_t blk = (blk_t)[obj objectAtIndex:0]; blk(); } return 0; }
此时Block为__NSMallocBlock__。不仅对于Block可以copy,对于Block变量同样可以。这个稍后详细说明。
根据Block存储域,将copy方法进行赋值的动作总结一下为:
Block的类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用记数增加 |
不管Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。但在ARC中不可显示的release,copy没影响。
__block变量存储域
对于Block从栈复制到堆时,__block也会收到相应影响。如下表。
__block变量的配置存储域 | Block从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
若在一个Block中使用__block变量,则当该Block从栈复制到堆上时,使用的所有__block变量也必定配置在栈上,将会全部从栈复制到堆。此时,Block持有__block变量。即使该Block已复制到堆的情形下,复制Block也对所使用的__block变量没有任何影响。如下图。
在多个Block中使用__block变量时,因为最先会将所有的Block配置在栈上,所以__block变量也会配置在栈上。在任何一个Block从栈复制到堆时,__block变量也会一并从栈复制到堆并被该Block所持有。当剩下的Block从栈复制到堆时,被复制的Block持有__block变量,并增加__block变量的引用记数。
如果配置在栈上的Block被废弃,那么它所使用的__block变量也就被释放。
到这里可以看出,此思考方式与Object-C的引用计数式内存管理方式完全相同。使用__block变量的Block持有__block变量。如果Block被废弃,它所持有的__block变量也就被释放。
这里,理解下使用__block变量用结构体成员变量__forwarding的原因。 “不管__block变量配置在栈上还是堆上,都能够正确的访问该变量“。通过Block的复制,__block变量也从栈复制到堆,此时可同时访问栈上的__block变量和堆上的__block变量。源代码如下。
__block int var = 0; void (^blk)() = ^{ ++var; }; blk(); NSLog(@"%d", var);
利用copy方法复制使用了__block变量的Block语法。Block和__block变量两者均是从栈复制到堆。此代码中在Block语法的表达式中使用初始化后的__block变量。
^{++var;};
然后在Block语法之后使用与Block无关的变量。
++var;
以上俩种源代码均可转换为如下形式:
++(var.__forwarding->var);
查看下面源代码。
int main(int argc, const char * argv[]) { @autoreleasepool { int a = 123; __block int b = 123; NSLog(@"&a = %p, &b = %p", &a, &b); void(^block)() = ^{ NSLog(@"&a = %p, &b = %p", &a, &b); }; block = [block copy]; block(); NSLog(@"&a = %p, &b = %p", &a, &b); [block release]; } return 0; }
上述源代码输出为
&a = 0x7fff5fbff7ac, &b = 0x7fff5fbff7a0 &a = 0x100300028, &b = 0x100300088 &a = 0x7fff5fbff7ac, &b = 0x100300088
可以看到,在block执行中,他所引用的变量a和b都被复制到了堆上。而被标记__block的变量事实上应该说是被移动到了堆上,因此,当block执行后,函数栈内访问b的地址会变成堆中的地址。而变量a,仍会指向函数栈内原有的变量a的空间。
在变换Block语法的函数中,该变量var为复制到堆上的__block变量用结构体实例,而使用的与Block无关的变量var,为复制前栈上的__block变量用结构体实例。
但是栈上的__block变量用结构体实例在__block变量从栈复制到堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量用结构体实例的指针。如图所示。
通过该功能,无论是在Block语法中、Block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利的访问同一个__block变量。
截获对象
以下源代码生成并持有 NSMutableArray 类的对象,由于附有 __strong 修饰符的赋值目标变量作用域立即结束,因此对象被立即释放并废弃。
{ id array = [[NSMutableArray alloc] init]; }
再看Block语法中使用该变量array的代码:
typedef void (^blk_t)(id); blk_t blk; { id array = [[NSMutableArray alloc] init]; blk = [^ (id obj){ [array addObject:obj]; NSLog(@"%p", &array); NSLog(@"array count = %ld", [array count]); } copy]; } blk(@1); blk(@2); blk(@3);
变量作用域结束的同时,变量array被废弃,其强引用失效,因此赋值给变量array的 NSMutableArray 类的对象必定被释放并废弃。但该源代码运行正常,其执行结果如下:
0x100206990 array count = 1 0x100206990 array count = 2 0x100206990 array count = 3
这一结果意味着赋值给array的 NSMutableArray 类的对象在该源代码最后Block的执行部分超出其变量作用域而存在。通过编译器转换后的源代码发现:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/); }
__main_block_copy_0 函数使用 _Block_object_assign 函数将对象类型赋值给Block结构体的成员变量array中并持有该对象。
_Block_object_assign 函数调用相当于retain实例方法的函数,将对象赋值在对象类型的结构体成员变量中。
另外,__main_block_dispose_0 函数使用 _Block_object_dispose 函数,释放赋值在Block结构体成员变量中的对象
static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
__main_block_dispose_0 函数相当于 release 实例方法的函数,释放赋值在对象类型的结构体成员变量中对象。
在调用Block的copy函数实例方法时,如果Block配置在栈上,那么该Block会从栈复制到堆。Block作为函数返回值返回时、将Block赋值给 __strong 修饰符id类型的类或Block类型成员变量时,编译器自动将对象的Block作为参数并调用 _Block_copy 函数,这与直接调用Block的copy实例方法的效果相同。
copy函数和dispose函数能够截获对象和__block变量,通过俩种类型来区分。
对象 | BLOCK_FIELD_IS_OBJECT |
---|---|
__block变量 | BLOCK_FIELD_IS_BYREF |
循环引用
循环引用是指本类中含有block,同时block还有对self的引用。一般在自己写的API中需要处理循环引用,在系统API中,要视情况而定。类似下面这种就是不需要的。
[UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }]; [[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * notification) { self.someProperty = xyz; }];
但如果你使用一些参数中可能含有 ivar 的系统 api ,如 GCD 、NSNotificationCenter就要小心一点:比如GCD 内部如果引用了 self,而且 GCD 的其他参数是 ivar,则要考虑到循环引用:
__weak __typeof__(self) weakSelf = self; dispatch_group_async(_operationsGroup, _operationsQueue, ^ { __typeof__(self) strongSelf = weakSelf; [strongSelf doSomething]; [strongSelf doSomethingElse]; });
类似的:
__weak __typeof__(self) weakSelf = self; _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey" object:nil queue:nil usingBlock:^(NSNotification *note) { __typeof__(self) strongSelf = weakSelf; [strongSelf dismissModalViewControllerAnimated:YES]; }];
self –> _observer –> block –> self 显然这也是一个循环引用。
测试
经过上面的分析,对于Block的了解基本差不多了,最后在附上一盘Block的小测试。
参考
在学习的过程中,查阅了以下文章,一起分享给大家。
- 《Object-C》高级编程
- 谈Objective-C Block的实现
- A look inside blocks: Episode 1
- A look inside blocks: Episode 2
- A look inside blocks: Episode 3
- Objective-C Blocks Quiz