##一、 概述
总的来说,Block = 匿名函数 + Capture 变量。和 Java 中的 Lambda 表达式类似。它的用途包括:
- 作为匿名函数使用,即把函数定义在使用到的地方,逻辑更加集中。RAC 就有很多这样的应用。
- Capture 变量,可以 Capture Block 定义所在的作用域内的变量,类似于保存上下文。之后,Block 在别处被调用时,就好像 Block 还在原本上下文一样,可以正常执行。这些都是 OC 自动做的事情。而我们在定义 Block 的时候,当前作用域内的变量,都可以在 Block 中使用。不需要顾及当 Block 在别处被调用,当前上下文已经不存在的问题。当然你需要修改变量的话,需要使用
__block
显式声明一下。
二、Block 的类型 (Stack Block VS Malloc Block VS Global Block)
Block 是特殊的 OC 对象,是可以在 Stack 上创建的对象。在 Stack 上创建 Block 是为了运行效率而做的优化。
我们知道,一般来说,栈上变量的分配、回收、访问速度要高于堆上变量,一是栈上数据会频繁创建与销毁,创建与销毁的逻辑相对简单(从当前可用区域中划分一块儿给变量,栈向下增长,可用空间的起始地址变小一点),而堆上创建销毁逻辑相对复杂(有很多算法,大小最适合的空间还是第一块可用的空间等等),一是栈帧数据会被频繁访问、修改,数据很大可能性已经映射到处理器缓存中,二是堆数据是线程共享数据,访问堆上数据可能会有线程同步开销,当然这不是一定的。
所以,如果一个 Block 只在当前作用域内使用,可以在 Stack 上创建,速度更快。使用 StackBlock 也有要小心的地方,StackBlock 可以理解为局部变量,将会随栈帧的销毁而销毁,retain 不起作用,因为 StackBlock 是栈变量,它的回收不依赖引用计数机制。如果需要,可以调用 Block 的 copy 方法,把 StackBlock 变成 MallocBlock , MallocBlock 的内存位于堆上。
没有 Capture 任何变量的 Block 是 Global Block 。 Global Block 存储在特殊的静态区域,全局唯一,不会被 dispose ,不会被 copy(并不了解内幕,MallocBlock 的 copy 也并没有新开辟一块儿内存给新的实例)。
##三、 __block 是干什么的
简单来说,
- 使用__block修饰变量,block将capture变量的地址。
- 没有使用__block修饰变量,block仅capture变量的值。
在block内,不能修改没有使用 __block 修饰的 capture 变量。原因是修改是无效的。为了避免难以发现的 bug,编译器会报错,如果真的想要修改,就使用 __block 修饰变量。因为没有使用 __block 修饰变量, block 仅 capture 变量的值。在 block内,修改没有使用 __block 修饰的capture变量,类似于下面这种情形。
- (void)test:(id) foo{
foo = nil;
}
...
Foo *foo = [Foo new];
NSLog(@"%@", foo);
[self test:foo];
NSLog(@"%@", foo);
...
//output
2017-05-07 10:05:01.887 TestBlock[53766:7428855] <Foo: 0x618000005800>
2017-05-07 10:05:01.887 TestBlock[53766:7428855] <Foo: 0x618000005800>
(此外,使用__block修饰的变量在block内外的修改对彼此都可见,因为都dereference了。)使用__block修饰变量会产生一次relocate,变量将被relocate到堆上。验证如下:
...
int x = 0;
__block Foo *foo = [Foo new];
NSLog(@"%p", &x);
NSLog(@"%p", &foo);
void(^block1)(void) = ^(){
NSLog(@"%@", foo);
};
NSLog(@"%p", &foo);
...
//output
2017-05-07 10:21:10.288 TestBlock[53977:7470636] 0x7fff52a7fa3c
2017-05-07 10:21:10.288 TestBlock[53977:7470636] 0x7fff52a7fa30
2017-05-07 10:21:10.288 TestBlock[53977:7470636] 0x618000248398
(从中,我们也能得知,relocate不是发生在变量声明时,而是发生在变量在block中被使用时。这么做是合理的,使用__block修饰而实际并未在任何block中使用的变量,是不需要relocate的。)
为什么需要 relocate ?因为栈是临时的,block 可能生命周期很长。 block 的生命周期可能长于当前栈帧(比如使用分发到 GCD 执行)。也就是说, block 可能在栈帧被销毁之后某个时间执行。这要求 block 不能 capture 一个栈上的地址。一但 capture ,并且使用时栈帧已被销毁,就会发生 bad_access 错误。
##四、 block 的 c 实现(待补充)
参考:iOS 开发进阶 by 唐巧