重学 Block & Closure 系列之 Block 篇 | Soledad
重学 Block & Closure 系列是关于 Objective-C 中的 Block(块) 和 Swift 中的 Closure(闭包)。
我总是想弄清楚一些事情并把它们记录下来,那样会帮助我更好的掌控它们,自己每天也会更加安心。
本文即是关于 Block 的个人小结。另一篇关于 Closure 的在这里。
基本用法
Block 最早是和 GCD 一起被苹果公司引入的,最初主要是用来解决多线程编程的问题。Block 十分有用,借由它,开发者可以将代码像对象一样传递,令其在不同环境下运行。还有一个关键点,在定义 Block 的范围内,它可以访问到其中的全部变量。
Block 的声明格式如下:
return_type (^block_name)(parameters)
格式看上去有点奇怪。因为正常的声明变量都是 NSString *str; ,变量类型在左,变量名在右。而 Block 的变量名在中间,这一点值得注意一下。
来看一段例子:
1 2 3 | void (^someBlock)() = ^{ }; |
这段代码中定义了一个名为 someBlock 的 Block。它不接受参数,返回类型为 void。
例子二:
1 2 3 | int (^addBlock)(int a, int b) = ^(int a, int b) { return a + b; }; |
定义好之后,就可以像函数那样使用了:int add = addBlock(3, 5)
Block 的威力还不止于此,在它声明的范围里,所有变量都可以为其所捕获。例如:
1 2 3 4 | int additional = 5; int (^addBlock)(int a, int b) = ^(int a, int b) { return a + b + additional; }; |
默认情况下,被 Block 所捕获的变量,是不可以在块内被修改的。如果想要修改,那么必须在声明变量的前面加上 __block 关键字。(实例变量除外,不加 __block 同样可以被修改,具体见下段)
另外,如果被 Block 捕获的变量是对象类型,那么会自动保留它。系统在释放这个块的时候,也会将其一并释放。
特别的,如果将块定义在 Objective-C 类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用 self 变量。块总能修改实例变量,所以在声明时不需要加 __block。看个例子:
1 2 3 4 5 | -(void)instanceMethod { void(^someBlock)() = ^{ _anInstanceVarible = @"Something"; } } |
分析一下,如果这时某个实例正在执行该实例方法,那么 self 变量就指向此实例,从而引用了 someBlock。另外,_anInstanceVarible 实际上是 self -> _anInstanceVarible。因此,someBlock 捕获了 self。因此会出现循环引用的情形。
内部结构

1 2 3 4 5 6 7 8 9 10 11 12 13 14 | struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *); }; struct Block_layout { void *isa; // 所有对象都有的 isa 指针 int flags; int reserved; void (*invoke)(void *, ...); // 指向具体的 block 实现的函数调用地址 struct Block_descriptor *descriptor; // 附件描述信息 /* Imported variables. */ // Block 捕获来的变量 }; |
值得一提的有,对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的,如下图所示:

对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的,如下图所示:

Block 的类型
栈块
定义 Block 的时候,其所占的内存区域是分配在栈中的。也就是说,Block 只在定义它的那个范围内有效。例如,下面的代码就很危险:
1 2 3 4 5 6 7 8 9 10 11 | void (^block)(); if (/* some condition */) { block = ^{ NSLog(@"Block A"); } } else { block = ^{ NSLog(@"Block B"); } } block(); |
定义在 if或else 语句中的两个块都是分配在栈内存中的。等离开了相应的范围(if或else语句)之后,编译器有可能把分配的内存复写掉(栈内存本身也会自动回收)。
堆块
解决的方法是,可以给块对象发送 copy 消息,将块从栈上复制到堆上。拷贝后的块可以在超出它的范围之外安全使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。之后的复制操作都不会真的复制,只是会增加引用计数。代码:
1 2 3 4 5 6 7 8 9 10 11 | void (^block)(); if (/* some condition */) { block = [^{ NSLog(@"Block A"); } copy]; } else { block = [^{ NSLog(@"Block B"); } copy]; } block(); |
全局块
还有一种 Block ,不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。块所使用的内存区域,在编译时就已经完全确定了,因此,全局块声明在全局内存里,而不需要每次用到的时候在栈中创建。另外,对其拷贝的操作是个空操作,因为全局块绝不可能为系统所回收。这种块实际上相当于单例。例:
1 2 3 | void (^block)() = ^{ NSLog(@"Block A"); }; |
由于运行该块的全部信息都能在编译期确定,所以可把它做成全局块。这完全是一种编译器级别上的优化。
另外,有关 Block 类型的讨论,建议看这篇实践文章,写的很仔细。
为常用的块类型创建 typedef
一般声明与定义 Block 的做法是这样:
1 2 3 | int (^blockName)(BOOL flag, int value) = ^(BOOL flag, int value) { return someInt; }; |
显然,这种做法很麻烦,并且由于 blockName 在中间,也不易读。比方说在这里,如果我们要表示接受 BOOL 以及 int 参数并返回 int 值的块,可以这样:
1 | typedef int(^SomeBlock)(BOOL flag, int value); |
这样相当于向系统中新增了一个名为 SomeBlock 的类型。以后,创建变量的方式改用这样就行了:
1 2 3 | SomeBlock blockName = ^(BOOL flag, int value){ // Implementation }; |
拿平时最常用的 completionHandler 来举例。如果不用 typedef,那么 completionHandler 会是这个样子:
1 | - (void)startWithCompletionHandler:(void(^)(NSData *data, NSError *error))completion; |
类似的,我们可以使用 typedef 改写:
1 2 | typedef void(^SomeCompletionHandler)(NSData *data, NSError *error); - (void)startWithCompletionHandler:(SomeCompletionHandler)completion; |
参考文献
关注我
- 微博:@CaiYue_
- GitHub: caiyue1993
- 邮箱:yuecai.nju@gmail.com
本文版权所有,如需转载,请告知原作者并注明出处