本文介绍一下 iOS 中 Block 相关内容,总结 Block 相关的使用方法和注意事项。
Block 概述
Block 也被称作闭包,是不带有名称的函数,匿名函数。相当于是一个代码块,把想要执行的代码封装在代码块里,等需要的时候调用。
Block 表达式语法:
返回值类型(^变量名)(参数) = ^返回值类型(参数){ 表达式 }
int (^myBlock1)(int) = ^(int num) { return num + 1; };
void (^myBlock2)(void) = ^(void){ NSLog(@”无参数,无返回值”); };
Block 常见用法
在 OC 中经常会用到 Block。使用 Block 方便快捷,集中代码块,适用于轻便、简洁的回调,如网络传输等。下面介绍几种常见的用法:
1、声明为属性
可将 block 声明为某个类的属性,在别的地方初始化赋值之后,等待触发回调传值等操作。
1 | @property (nonatomic, copy) void(^blockName)(NSInteger type); |
2、作为方法参数调用
当有时调用一个耗时的方法处理,不会立即返回,需要时间进行处理,当处理完成后告知调用者处理完毕,可进行后面的操作,在这样的情景下就可使用 block,例如网络请求回调。
1 | - (void)doSomethingParameters:(id)parameters completion:(void (^)(NSInteger type))completion { |
Block 的实质
Block 其实是作为 C语言的代码来进行处理的,通过编译器将 Block 语法转换为相关的 C语言代码,然后进行编译执行。可以通过 clang 来将 OC 代码转换为 C/C++ 代码,执行下面命令:
clang -rewrite-objc main.m
然后生成 main.cpp 文件。可以看到,简单的几行代码,转化为一堆 C语言代码:
1 | int main(int argc, const char * argv[]) { |
上面的代码其实就是,将 Block 的匿名函数作为 C语言的函数来处理。
就是使用函数指针调用函数,将函数 _main_block_func_0(__cself) 赋值给 impl.FuncPtr, 然后调用 blk->impl.FuncPtr(blk),参数 __cself 就是指向 blk 自身的指针。
Block 就是 OC 对象
OC 中由类生成对象,就是由该类生成对象的各个结构体,通过对象的成员变量 isa 指针指向该类的结构体实例 objc_class 继承自 objc_object,该结构体实例持有声明的成员变量、方法的名称、方法的实现(函数指针)、属性以及父类的指针。具体内容请参考之前的文章 Objective-C对象解析
Block 就是 OC 对象。 __main_block_impl_0 结构体相当于,基于 objc_object 结构体的 OC 对象的结构体。
值得注意的是,impl.isa = &_NSConcreteStackBlock;
_NSConcreteStackBlock 相当于一个结构体实例,将 Block 作为 OC 对象处理时,关于该类的信息就放在 _NSConcreteStackBlock 中。
Block 截获变量
如下面例子,定义一个 Block,在代码块里打印变量 a,然后修改后调用 Block 打印出的变量 a 的值不变。
1 | NSInteger val = 10; |
为何修改后打印 a 的值不变,因为 Block 语法的表达式使用的是它之前声明的局部变量 a。Block 表达式截获所使用的局部变量的值,保存了该变量的瞬时值。所以在第二次执行 Block 表达式时,即使已经改变了局部变量 a 的值,也不会影响 Block 表达式在执行时所保存的局部变量的瞬时值。
这就是 Block 变量截获局部变量值的特性。
通过上面的方法转换为 C语言,可以看到在 Block 中使用外面的变量,变量被作为成员变量追加到 __main_block_impl_0 结构体中了。
从上图中可看到,截获自动变量,就是在表达式中所使用的变量被保存到 Block 的结构体中 __cself->val ,所以在 Block 之外修改变量 val 的值,Block 表达式里面的 val 值不变。
如果变量是对象类型的话,如下图所示:
在外面修改 obj 对象后, block 中的 obj 变量也是不会变化的。因为截获自动变量后,block 里的 obj 和外面的 obj 不是同一个指针了。
值得注意的是,如不在外面修改 obj 对象的话,block 中 obj 和外面的 obj 指向的是同一个地址,修改后外面的 obj 才指向新的地址。
转换为 C 语言,截获基本变量和截获对象对比可看到,多了两个函数,__main_block_copy_0 和 __main_block_dispose_0, __main_block_copy_0 的作用是将截获的对象复制给 Block 结构体的成员变量,持有对象,这样截获的对象就能够超出其变量作用域而存在。下文说到 Block 存储域的时候还会提到。
另外,在使用 C语言数组时要注意,截获自动变量的方法没有实现对 C 数组的截获,可使用指针来解决。
如上图所示,为何会报错呢?
通过上面截获变量的例子可知,变量被赋值给 Block 结构体中的成员变量,因为 C语言数组类型变量不能赋值给数组类型变量:char a[10]={‘a’}; char b[10]=a; 这样编译不能通过,所以会报错。
__block 说明符
在 Block 中只能使用保存的局部变量的瞬时值,并不能直接对其进行修改,想要修改需要在局部变量前加 __block 修饰。
1 | __block NSInteger val = 10; |
__block 类似于 static、auto,用于指定将变量值设置到哪个存储域中。auto 表示作为自动变量存储在栈中,static 表示作为静态变量存储在数据区中。
继续转换为 C语言来看一下,如下图:

可看到,使用在变量前加上 __block 说明符后,代码增加了很多,__block 变量变成了*__Block_byref_val_0* 结构体类型的变量。
1 | struct __Block_byref_val_0 { |
1 | __Block_byref_val_0 *val = __cself->val; // bound by ref |
其中结构体的成员变量 *__Block_byref_val_0 __forwarding 是指向自身的指针,通过这个指针来访问变量 __forwarding->val。为何这样使用,下面来介绍。
Block 存储域
前面说过,Block 是作为 OC 对象处理的,impl.isa = &_NSConcreteStackBlock; 该类的信息就放在 _NSConcreteStackBlock 中。
除了 _NSConcreteStackBlock 类型,还有其他两种类型 _NSConcreteGlobalBlock 和 _NSConcreteMallocBlock,这三类所在内存存储区域有区别:
- _NSConcreteStackBlock 栈区
- _NSConcreteGlobalBlock 数据区域
- _NSConcreteMallocBlock 堆区
_NSConcreteGlobalBlock
前面出现的 Block 例子,使用的是 _NSConcreteStackBlock 设置在栈上。
在全局变量使用 Block 时,生成的是 _NSConcreteGlobalBlock 类对象,如下:
1 | void (^blockName)(void) = ^{ NSLog(@"block"); }; |
因为在使用全局变量的地方不能使用自动变量,所以就不存在堆自动变量进行截获。此种类型的 Block 结构体实例内容不依赖于执行的状态,所以整个程序只需一个实例,将 Block 用结构体实例设置在与全局变量相同的数据区域中即可。
总结来说就是,在全局变量有 Block 语法时,Block 语法的表达式不使用截获的自动变量时,Block 为 _NSConcreteGlobalBlock 类对象。
_NSConcreteMallocBlock
配置在全局变量上的 Block 在变量作用域外也可以通过指针访问使用,但是配置在栈上的 Block 若其所在变量作用域结束,该 Block 就被废弃,同样的 __block 变量也配置在栈上,若所在变量作用域结束,则该 __block 变量也会被废弃。
因此提供了将 Block 和 __block 变量从栈上复制到堆上的方法来解决这个问题,从栈上复制到堆上,即使变量作用域结束,堆上的 Block 还可以继续存在。
复制到堆上的 Block 将 _NSConcreteMallocBlock 类对象写入到 Block 结构体实例的成员变量 isa: impl.isa = &_NSConcreteMallocBlock;
__block 变量的结构体成员变量 __forwarding 可以实现无论 __block 变量配置在栈上,还是堆上都能正确的访问 __block 变量。
在 ARC 模式下,编译器会判断,自动生成将 Block 从栈上复制到堆上的代码。但是当,向方法或函数的参数中传递 Block 时,需要手动复制,如下面的代码。
1 | - (NSArray *)getBlockArray { |
如果在方法或函数中复制了传递过来的参数,那么就不必再调用该方法或函数前手动复制了,例如,在方法命中含有 usingBlock 时,[array enumerateObjectsUsingBlock:…],或者 GCD 的 API 中,不用手动复制。
当对 Block 调用 copy 方法时,_NSConcreteStackBlock 类会从栈复制到堆上。_NSConcreteGlobalBlock 类什么也不做。_NSConcreteMallocBlock 类引用计数器增加。
什么时候栈上的 Block 会复制到堆上:
- 调用 Block 的 copy 实例方法时
- Block 作为函数返回值返回时
- 将 Block 赋值给有 __strong 修饰符 id 类型的类或 Block类型变量时
- 在方法名含有 usingBlock 的 Coca 框架方法 或 GCD 的 API 中传递 Block 时
注意下面几种情况,ARC下:
1 |
|
1 | __block NSInteger val = 3; |
1 |
|
__block 变量和对象
当 Block 从栈复制到堆时, __block 变量也全部被从栈复制到堆并被 Block 所持有。
若有多个 Block 使用同一个 __block 变量时,任何一个 Block 从栈复制到堆时,__block 变量也会从栈复制到堆,剩下的 Block 从栈复制到堆,被复制的 Block 持有 __block 变量并增加 __block 变量的引用计数。
若配置在堆上的 Block 被废弃,它所使用的 __block 变量也会被释放。
前面提到 __block 变量的结构体成员变量 __forwarding 可以实现无论 __block 变量配置在栈上,还是堆上都能正确的访问 __block 变量。如下例子:
1 | __block NSInteger val = 10; |

__block 变量从栈上复制到堆上,此时会将成员变量 __forwarding 的值替换为复制到堆上的 __block 变量结构体实例的地址。val.__forwarding 使用的是同一个在堆上的值,从而能保证正确的访问同一个 __block 变量。
上图中有两个函数,__main_block_copy_0 和 __main_block_dispose_0,在 Block 复制到堆上和从堆上释放时被调用。将使用到的 __block 变量或者截获的对象复制给 Block 结构体的成员变量,持有对象。这样截获的对象就能够超出其变量作用域而存在。通过参数 BLOCK_FIELD_IS_BYREF 和 BLOCK_FIELD_IS_OBJECT 来区分函数对象类型是 __block 变量还是对象。
1 | id array = [NSMutableArray array]; |
Block 的循环引用问题
如在 block 中使用了对象, block 会对使用的对象进行持有,如该对象同时持有该 block 则会造成循环引用的问题,互相持有不能释放。
1 |
|
1 |
|
__weak、 __strong 的使用
声明一个对象:
1 | id __strong obj = [[NSObject alloc] init]; |
1 | __weak id weakObj = obj; |
有关底层实现可查看 clang 文档 http://clang.llvm.org/docs/AutomaticReferenceCounting.html
weak 表是用Hash table实现的, objc_storeWeak 函数就把第一个入参的变量地址注册到weak表中,然后根据第二个入参来决定是否移除。如果第二个参数为0,那么就把 __weak变量从weak表中删除记录,并从引用计数表中删除对应的键值记录。
如果 __weak 引用的原对象如果被释放了,那么对应的 __weak 对象就会被指为nil。就是通过 objc_storeWeak 函数这些函数来实现的。
我们已经知道使用 weakSelf 来解决循环引用的问题,为何有的还需要在 block 里使用 strongSelf ?
1 | __weak __typeof(self) weakSelf = self; |
有的情况下 block 会延迟调用,在 block 未调用前 self 可能 已经释放掉了,这时再在 block 使用 weakSelf ,weakSelf 为空了。在 block 里面使用的 __strong 修饰的 weakSelf 是为了在函数生命周期中防止 self 提前释放。strongSelf 是一个自动变量当 block 执行完毕就会释放自动变量 strongSelf ,不会对 self 进行一直进行强引用。
另外要注意的是,有些情况在 block 未执行的时候,self 就已经释放掉了,block 在执行时 strongSelf 还是 nil,为了安全,最好还是在执行时判断 strongSelf == nil.
总结来说就是,weakSelf 是为了 block 不持有 self,避免循环引用。strongSelf 防止 block 在执行的过程中 self 提前释放。