1. OC中block的用法
iOS 中block被用于嵌套方法间传递,并在方法调用返回时,通过block回调返回值。声明与定义形式如下:
标准声明与定义
returnType (^blockName)(var_type) = ^ returnType (varType varName) { ... };
blockName(var);
当返回类型为void
void (^blockName)(var_type) = ^void (varType varName) { ... };
blockName(var);
可省略写成
void (^blockName)(var_type) = ^(varType varName) { ... };
blockName(var);
当参数类型为void
returnType (^blockName)(void) = ^ returnType (void) { ... };
blockName();
可省略写成
returnType (^blockName)(void) = ^ returnType { ... };
blockName();
当返回类型和参数类型都为void
void (^blockName)(void) = ^void (void) { ... };
blockName();
可省略写成
void (^blockName)(void) = ^{ ... };
blockName();
匿名Block
Block实现时,等号右边就是一个匿名Block,它没有blockName,称之为匿名Block:
^ returnType (varType varName) { ... };
2. 函数指针
通过指针调用函数,看起来没用到函数名,也被称为匿名函数调用。
C语言中,函数定义是这样:
int func(int count);
调用时,是这样:
int result = func(10);
func就是它的函数名。
我们可以获取函数指针:
int (*funcptr)(int) = &func;
再像下面这样匿名调用函数,即不带函数名的调用:
int result = (*funcptr)(10);
可看得出来,Block 就类似于使用匿名函数调用。
3. block的实现原理
使用Block的时候,编译器对Block语法进行了怎样的转换?
int main() {
int count = 10;
void (^ blk)() = ^(){
NSLog(@"In Block:%d", count);
};
blk();
}
经clang命令clang -rewrite-objc main.m转换后,会在当前目录生成main.cpp文件,可得到以下几个部分(有代码删减和注释添加):
static void __main_block_func_0(
struct __main_block_impl_0 *__cself) {
int count = __cself->count; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_d2f8d2_mi_0,
count);
}
这是一个函数的实现,对应 Block 中 { } 内的内容,这些内容被当做了C语言函数来处理,函数参数中的 __cself 相当于 OC 中的 self。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc; //描述Block大小、版本等信息
int count;
//构造函数
__main_block_impl_0(void *fp,
struct __main_block_desc_0 *desc,
int _count,
int flags=0) : count(_count) {
impl.isa = &_NSConcreteStackBlock; //在函数栈上声明,则为_NSConcreteStackBlock
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0 即为 main()函数栈上的Block结构体,其中的 __block_impl 结构体声明如下:
struct __block_impl {
void *isa;//指明对象的Class
int Flags;
int Reserved;
void *FuncPtr;
};
__block_impl 结构体,即为Block的结构体,可理解为Block的类结构。
再看下 main 函数翻译的内容:
int main() {
int count = 10;
void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
去除掉复杂的类型转化,可简写为:
int main() {
int count = 10;
sturct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0, //函数指针
&__main_block_desc_0_DATA)); //Block大小、版本等信息
(*blk->FuncPtr)(blk); //调用FuncPtr指向的函数,并将blk自己作为参数传入
}
由此,可以看出,Block也是Objective-C中的对象,其也有isa指针,此处指向了&_NSConcreteStackBlock。
Block有三种类(即__block_impl的isa指针指向的值,isa说明参考《Objective-C isa 指针 与 runtime 机制》),根据Block对象创建时所处数据区不同而进行区别:
_NSConcreteStackBlock:在栈上创建的Block对象,
_NSConcreteMallocBlock:在堆上创建的Block对象,
_NSConcreteGlobalBlock:全局数据区的Block对象。
4. 关于block应用的几点说明
4.1 自动变量的截获
在第3节介绍的代码中,__main_block_impl_0 结构体(main栈上Block的结构体)的构造函数中可以看到,栈上的变量 count 以参数的形式传入到了这个构造函数中,即为变量的自动截获。
- __block_impl结构体已经可以代表Block类了,但在栈上又声明了__main_block_impl_0结构体,对__block_impl进行封装后才来表示栈上的Block类,就是为了获取Block中使用到的栈上声明的变量(栈上没在Block中使用的变量则不会被捕获),变量被保存在Block的结构体实例中。
- 在blk()执行之前,栈上简单数据类型的count无论发生什么变化,都不会影响到Block以参数形式传入而捕获的值。
- 若变量是指向对象的指针时,是可以修改这个对象的属性的,只是不能为变量重新赋值。
4.2 Block的存储域
上文已提到,根据Block创建的位置不同,Block有三种类型,创建的Block对象分别会存储到栈、堆、全局数据区域。
void (^blk)(void) = ^{
NSLog(@"Global Block");
};
int main() {
blk();
NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
}
像上面代码块中的全局blk自然是存储在全局数据区,但注意在函数栈上创建的blk,如果没有截获自动变量,Block的结构实例还是会被设置在程序的全局数据区,而非栈上:
int main() {
void (^blk)(void) = ^{//没有截获自动变量的Block
NSLog(@"Stack Block");
};
blk();
NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
int i = 1;
void (^captureBlk)(void) = ^{//截获自动变量i的Block
NSLog(@"Capture:%d", i);
};
captureBlk();
NSLog(@"%@",[captureBlk class]);//打印:__NSMallocBlock__
}
可以看到截获了自动变量的Block打印的类是NSGlobalBlock,表示存储在全局数据区。
但为什么捕获自动变量的Block打印的类却是设置在堆上的NSMallocBlock,而非栈上的NSStackBlock?这个问题稍后解释。
4.3 Block复制
配置在栈上的Block,如果其所属的栈作用域结束,该Block就会被废弃,对于超出Block作用域仍需使用Block的情况,Block提供了将Block从栈上复制到堆上的方法来解决这种问题,即便Block栈作用域已结束,但被拷贝到堆上的Block还可以继续存在。
复制到堆上的Block,将_NSConcreteMallocBlock类对象写入Block结构体实例的成员变量isa:
impl.isa = &_NSConcreteMallocBlock;
在ARC有效时,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码,以下几种情况栈上的Block会自动复制到堆上:
- 调用Block的copy方法
- 将Block作为函数返回值时
- 将Block赋值给__strong修改的变量时
- 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
其它时候向方法的参数中传递Block时,需要手动调用copy方法复制Block。
上一节的栈上截获了自动变量i的Block之所以在栈上创建,却是NSMallocBlock类,就是因为这个Block对象赋值给了_strong修饰的变量captureBlk(_strong是ARC下对象的默认修饰符)。
因为上面四条规则,在ARC下其实很少见到_NSConcreteStackBlock类的Block,大多数情况编译器都保证了Block是在堆上创建的,如下代码所示,仅最后一行代码直接使用一个不赋值给变量的Block,它的类才是NSStackBlock:
int count = 0;
blk_t blk = ^(){
NSLog(@"In Stack:%d", count);
};
NSLog(@"blk's Class:%@", [blk class]);//打印:blk's Class:__NSMallocBlock__
NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__
NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__
NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打印:Stack Block:__NSStackBlock__
关于ARC下和MRC下Block自动copy的区别,查看《Block 小测验》里几道题目就能区分了。
另外,原书存在ARC和MRC混合讲解、区分不明的情况,比如书中几个使用到栈上对象导致Crash的例子是MRC条件下才会发生的,但书中没做特殊说明。
4.4 使用__block发生了什么
Block捕获的自动变量添加__block说明符,就可在Block内读和写该变量,也可以在原来的栈上读写该变量。
自动变量的截获保证了栈上的自动变量被销毁后,Block内仍可使用该变量。
__block保证了栈上和Block内(通常在堆上)可以访问和修改“同一个变量”,__block是如何实现这一功能的?
__block发挥作用的原理:将栈上用__block修饰的自动变量封装成一个结构体,让其在堆上创建,以方便从栈上或堆上访问和修改同一份数据。
验证过程:
现在对刚才的代码段,加上__block说明符,并在block内外读写变量count。
int main() {
__block int count = 10;
void (^ blk)() = ^(){
count = 20;
NSLog(@"In Block:%d", count);//打印:In Block:20
};
count ++;
NSLog(@"Out Block:%d", count);//打印:Out Block:11
blk();
}
将上面的代码段clang,发现Block的结构体__main_block_impl_0结构如下所示:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_count_0 *count; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
最大的变化就是count变量不再是int类型了,count变成了一个指向__Block_byref_count_0结构体的指针,__Block_byref_count_0结构如下:
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding;
int __flags;
int __size;
int count;
};
它保存了int count变量,还有一个指向__Block_byref_count_0实例的指针__forwarding,通过下面两段代码__forwarding指针的用法可以知道,该指针其实指向的是对象自身:
//Block的执行函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_count_0 *count = __cself->count; // bound by ref
(count->__forwarding->count) = 20;//对应count = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_0,
(count->__forwarding->count));
}
//main函数
int main() {
__attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,
(__Block_byref_count_0 *)&count,
0,
sizeof(__Block_byref_count_0),
10};
void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,
&__main_block_desc_0_DATA,
(__Block_byref_count_0 *)&count,
570425344));
(count.__forwarding->count) ++;//对应count ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_1,
(count.__forwarding->count));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
为什么要通过__forwarding指针完成对count变量的读写修改?
为了保证无论是在栈上还是在堆上,都能通过都__forwarding指针找到在堆上创建的count这个__main_block_func_0结构体,以完成对count->count(第一个count是__main_block_func_0对象,第二个count是int类型变量)的访问和修改。
示意图如下:

4.5 Block的循环引用
Block的循环引用原理和解决方法大家都比较熟悉,此处将结合上文的介绍,介绍一种不常用的解决Block循环引用的方法和一种借助Block参数解决该问题的方法。
Block循环引用原因:一个对象A有Block类型的属性,从而持有这个Block,如果Block的代码块中使用到这个对象A,或者仅仅是用用到A对象的属性,会使Block也持有A对象,导致两者互相持有,不能在作用域结束后正常释放。
解决原理:对象A照常持有Block,但Block不能强引用持有对象A以打破循环。
解决方法:
方法一: 对Block内要使用的对象A使用__weak进行修饰,Block对对象A弱引用打破循环。
有三种常用形式:
- 使用__weak ClassName
__block XXViewController* weakSelf = self;
self.blk = ^{
NSLog(@"In Block : %@",weakSelf);
};
- 使用__weak typeof(self)
__weak typeof(self) weakSelf = self;
self.blk = ^{
NSLog(@"In Block : %@",weakSelf);
};
- Reactive Cocoa中的@weakify和@strongify
@weakify(self);
self.blk = ^{
@strongify(self);
NSLog(@"In Block : %@",self);
};
其原理参考《@weakify, @strongify》,自己简便实现参考《@weak - @strong 宏的实现》
方法二:对Block内要使用的对象A使用__block进行修饰,并在代码块内,使用完__block变量后将其设为nil,并且该block必须至少执行一次。
__block XXController *blkSelf = self;
self.blk = ^{
NSLog(@"In Block : %@",blkSelf);
blkSelf = nil;//不能省略
};
self.blk();//该block必须执行一次,否则还是内存泄露
优点是:
- 可通过__block变量动态控制持有XXController对象的时间,运行时决定是否将nil或其他变量赋值给__block变量
- 不能使用__weak的系统中,使用__unsafe_unretained来替代__weak打破循环可能有野指针问题,使用__block则可避免该问题
其缺点也明显:
- 必须手动保证__block变量最后设置为nil
- block必须执行一次,否则__block不为nil循环应用仍存在
因此,还是避免使用第二种不常用方式,直接使用__weak打破Block循环引用。
方法三:将在Block内要使用到的对象(一般为self对象),以Block参数的形式传入,Block就不会捕获该对象,而将其作为参数使用,其生命周期系统的栈自动管理,不造成内存泄露。
即原来使用__weak的写法:
__weak typeof(self) weakSelf = self;
self.blk = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"Use Property:%@", strongSelf.name);
//……
};
self.blk();
改为Block传参写法后:
self.blk = ^(UIViewController *vc) {
NSLog(@"Use Property:%@", vc.name);
};
self.blk(self);
优点:
- 简化了两行代码,更优雅
- 更明确的API设计:告诉API使用者,该方法的Block直接使用传进来的参数对象,不会造成循环引用,不用调用者再使用weak避免循环
该种用法的详细思路,和clang后的数据结构,可参考《Heap-Stack Dance》。
网友评论