Blocks Blocks 是 C 语言的扩展功能,是带有自动变量(局部变量) 的匿名函数 。这一概念在其他程序语言中也称为闭包(Closure)、Lambda等。 C 语言标准不允许不带有名称的函数,想要不通过函数名来调用函数可以如下所示:
1 2 3 4 5 6 int func (int count) { return count + 1 ; } int (*funcptr)(int ) = &func;int result = (*funcptr)(10 );
但是其实在赋值函数指针时,还是用到了函数的名称。
C语言的函数中使用的变量有一下几种:
函数的参数 - 存储在栈中
自动变量(局部变量)- 存储在栈中
静态变量(静态局部变量)- 作用域与局部变量相同,但是改变了存储方式,从而改变了生存周期
全局变量
静态全局变量 - 与全局变量一样是静态存储方式,但是将其作用域限制在了定义该变量的源文件内
能够在函数的多次调用中传递值的变量有:静态局部变量、静态全局变量和全局变量,这几种变量虽然作用域不同,但是在整个程序中,一个变量总是保持在一个内存区域中,函数对其多次访问也是访问同一个值。
Blocks 提供了类似 C++ 和 Objective-C 类生成实例或对象来保持变量值的方法,它保持局部变量的值。同时避免了声明类的大量代码,也没有使用静态变量、静态全局变量或全局变量时,访问的总是同一个值的问题。
Blocks的语法 我们看一个简单的Block的定义:
1 2 3 ^int (int count){ return count + 1 ; };
其实它与一般的C语言函数只有两点不同:1、返回值类型前面带有”^”符合,2、返回值类型后面没有函数名。
1 2 3 int testFunc (int count) { return count + 1 ; };
我们在通常使用中会省略Block的返回值类型,所以上面的表达式通常写为:
1 2 3 ^(int count){ return count + 1 ; };
在省略返会在类型时,如果表达式中没有return
语句,就使用void
类型,有return
语句就使用该返回值的类型,若有多个return
语句,那么所有的语句返回值类型应该相同。 如果不使用参数的话,参数列表也可以省略。
Block类型变量 在 Block 语法下,可将 Block 语法赋值给声明为 Block 类型的变量中。即源代码中一旦使用 Block 语法就相当于生成了可赋值给 Block 类型变量的“值”。Block 类型变量完全可以与一般的 C 语言变量一样使用。 我们将上文中的 Block 赋值给 Block 类型的变量:
1 int (^blk)(int ) = ^(int count){return count + 1 ;};
但是在函数参数或返回值中使用 Block 类型变量时,记述起来很复杂,因此常用 typedef 来解决:
1 2 typedef int (^blk_t)(int );blk_t blk = ^(int count){return count + 1 ;};
Blocks的实现 Block的实质 Block的本质就是 C 语言结构体的实例,Block 就是 Objective-C 对象。 Block 的语法看上去很特别,但实际上是作为普通的 C 语言源代码来处理的。我们可以用 clang 编译器的“clang -rewrite-objc 源代码文件名”指令将源代码转化为我们可读的源代码。
下面我们看如下这段代码转化为 C++ 源代码:
1 2 3 4 5 6 7 int main(){ void (^blk)(void ) = ^{ printf("Block\n" ); }; return 0 ; }
转化后的 C++ 源代码:
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 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 ("Block\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 (*blk)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); return 0 ; }
Block截获自动变量值 Block 是带有自动变量(局部变量) 的匿名函数 ,”带有自动变量值”在 Block 中表现为截获 Block 中使用到的自动变量值。 我们看一个实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main(){ int dmy = 256 ; int val = 10 ; const char *fmt = "var = %d\n" ; void (^blk)(void ) = ^{ printf(fmt,val); }; val = 2 ; fmt = "These values were changed. var = %d\n" ; blk(); return 0 ; }
转化后的 C++ 源代码:
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 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 * Desc; const char *fmt; int val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0 ) : fmt (_fmt), val (_val) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { const char *fmt = __cself->fmt; int val = __cself->val; printf (fmt,val); } 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 dmy = 256 ; int val = 10 ; const char *fmt = "var = %d\n" ; void (*blk)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val)); val = 2 ; fmt = "These values were changed. var = %d\n" ; ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); return 0 ; }
__block说明符 实际上,自动变量值截获只能保存执行 Block 语法瞬间的值,保存后就不能在 Block 中改写该值。试图在 Block 中修改自动变量会导致编译错误。 如果我们要改变 Block 中截获的自动变量值,有以下两个方法:
使用 static 修饰自动变量值将其变为静态变量,修改其存储域。
使用 __block 修饰符
我们看一下下面的源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int global_val = 1 ;static int static_global_val = 2 ;int main(){ static int static_val = 3 ; void (^blk)(void ) = ^{ global_val *=1 ; static_global_val *=2 ; static_val *=3 ; }; return 0 ; }
转化后的 C++ 源代码:
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 int global_val = 1 ;static int static_global_val = 2 ;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; global_val *=1 ; static_global_val *=2 ; (*static_val) *=3 ; } 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 () { static int static_val = 3 ; void (*blk)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val)); return 0 ; }
可以看到对于静态变量,Block 会捕获其指针对其进行访问。这种方法似乎对于自动变量也可以,为什么不截获自动变量的指针呢?因为自动变量再其作用域结束时已经被废弃了,Block调用时再去通过指针访问会出错。
我们在来看看使用“__block存储域类说明符”的例子:
1 2 3 4 5 6 7 int main(){ __block int val = 10 ; void (^blk)(void ) = ^{ val = 1 ; }; return 0 ; }
转化后的 C++ 源代码:
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 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; __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; (val->__forwarding->val) = 1 ; } 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 );}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void *)src->val, 8 );}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_val_0 val = {(void *)0 ,(__Block_byref_val_0 *)&val, 0 , sizeof (__Block_byref_val_0), 10 }; void (*blk)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 0x22000000 )); return 0 ; }
__block 说明符将自动变量变成了栈上生成的结构体实例 __Block_byref_val_0,且这个结构体的指针被 Block 结构体所捕获。被捕获的 __block变量生成的结构体并不在Block用__main_block_impl_0结构体中,这样做是为了能在多个Block中使用同一个 __block变量。 __Block_byref_val_0 结构体实例的成员变量 __forwarding 持有该结构体实例自身的指针,通过这个 __forwarding 指针访问成员变量 val。 如下图所示
为什么要使用指向结构体自身的 __forwarding 指针来访问其val值呢?为什么 Block 作为返回值时可以超出其变量作用域而存在呢? 其实,Block 所属类有以下几种:
__NSConcreteStackBlock –该类的对象分配在栈上
__NSConcreteGlobalBlock –该类的对象分配在.data区
__NSConcreteMallocBlock –该类的对象分配在堆中
在记述全局变量的地方使用Block变量时,实际的Block对象类为 __NSConcreteGlobalBlock。还有一种情况当 Block 语法的表达式中不使用应截获的自动变量时,Block也是 __NSConcreteGlobalBlock 类对象。
Blocks提供了将 Block 和 __block 变量从栈上复制到堆上的方法来使 Block 可以超出其作用域而存在,这时候在堆上分配的 Block 就是 __NSConcreteMallocBlock 类对象。栈上的 Block 会在以下几种情况时复制到堆:
调用Block的 copy 方法时(除以下3种情况时均推荐自己调用copy方法,防止Block被废弃)
Block作为函数返回值返回时
将Block赋值给附有 __strong修饰符 id 类型的类或Block类型成员变量时
在方法名中含有 usingBlock的Cocoa框架方法或者GCD的API中传递Block时
将 Block 复制到堆之后,栈上的Block 的 __forwarding 指针指向了堆上的Block,从而保证了任何时候都可以正确的访问到val值。
Block截获对象 如果Block捕获的自动变量是 Objective-C 对象,会是什么情况呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 blk_t blk; { id array = [NSMutableArray new]; blk = [^(id object){ [array addObject:object]; NSLog (@"array count = %ld" ,[array count]); } copy ]; } blk([NSObject new]); blk([NSObject new]); blk([NSObject new]);
可以看到在作用域结束后,array并没有没废弃,这个对象被Block截获,并且在Block复制到堆时对其进行了retain操作,从而持有了这个对象。 我们看看上面代码的 C++版本:
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 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 * Desc; id __strong array; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0 ) : array (_array) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) { id __strong array = __cself->array; [array addObject:obj] NSLog (@"array count = %ld" ,[array count]); } 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 ); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void *)src->array, 3 ); } 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}; blk_t blk;{ id __strong array = [[NSMutalbleArray alloc] init]; blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000 )); blk = [blk copy]; } (*blk->impl.FuncPtr)(blk,[[NSObject alloc] init]); (*blk->impl.FuncPtr)(blk,[[NSObject alloc] init]); (*blk->impl.FuncPtr)(blk,[[NSObject alloc] init]);
在OC中,C语言结构体不能含有__strong修饰符的变量,因为编译器不知道合适进行C语言结构体的初始化喝废弃操作,不能很好的管理内存,但是运行时库能准确把我 Block 从栈复制到堆以及堆上的Block被废弃的时机,因此Block结构体中可以含有附有 __strong或__weak修饰的变量。
__block修饰对象 当我们想要在block中修改被捕获的对象时,可以用__block修饰,其代码如下:
1 __block id obj = [[NSObject alloc] init];
该代码经过clang转换后的__block变量结构体部分如下:
1 2 3 4 5 6 7 8 9 struct __Block_byref_obj_0 { void *__isa; __Block_byref_obj_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void *, void *); void (*__Block_byref_id_object_dispose)(void *); __strong id obj; };
在Block中使用__strong修饰符的对象类型自动变量的情况下,当Block从栈复制到堆时,使用__Block_object_assign函数,持有Block截获的对象。 需要特别注意的是如果对象是__weak修饰的,则不论是否有__block修饰,Block复制到堆时,均不持有该对象。
Block循环引用 在Block内部使用__strong修饰符的对象类型的自动变量时,当Block从栈复制到堆的时候,该对象就会被Block所持有。如果这个对象还同时持有Block的话,就容易发生循环引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 typedef void (^blk_t)(void );@interface Person : NSObject { blk_t blk; } @implementation Person - (instancetype )init { self = [super init]; blk = ^{ NSLog (@"self = %@" ,self ); }; return self ; } @end
Person 类对象持有Block类型成员变量blk,init实例方法中Block语法使用了附有__strong修饰符的对象self,并且由于Block语法赋值在了成员变量blk中,因此该栈上生成的Block会复制到堆,从而持有self,导致了循环引用。 可以使用__weak修饰符打破循环:
1 2 3 4 5 6 7 8 9 - (instancetype )init { self = [super init]; id __weak temp = self ; blk = ^{ NSLog (@"self = %@" ,temp); }; return self ; }
有时候也可以使用__block修饰符来打破循环:
1 2 3 4 5 6 7 8 9 10 11 12 13 - (instancetype )init { self = [super init]; __block id temp = self ; blk_= ^{ NSLog (@"self = %@" ,temp); temp = nil ; }; return self ; } - (void )execBlock { blk(); }
使用__block来打破循环的话,需要再block中将对象置为nil,并且保证block执行来打破循环。