block 是 Objective-C 中的重要技术。本文主要是写了一点对 block 应用的探究。
什么是 Block
首先补充几个概念和解释帮助理解,大神们直接略过:
- block 本质是一个结构体。
- 结构体可以创建在栈上,堆上,还可以在全局区,因此就有对应的三种 block:栈上的,堆上的和全局的。
如果 block 中没有捕捉变量,就是全局类型的, block 以及运行结果在编译期就能决定了,因此放在全局/静态区。
如果 block 捕捉了变量,就必须依赖于其创建时的上下文,并被储存在栈内,栈弹出之后就会失效(失效并不代表着执行 block 一定会失败)。
如果希望 block 的存活不再依赖于栈,就必须通过 copy 方法将 block 转化成堆类型的 block。堆类型的 block 保存在堆中,并带有引用计数,block 的引用计数和 Objective-C 对象的引用计数采用类似的原理,但并不是同一套实现,二者是有所区别的。
Block 怎样捕获变量
block 捕捉变量是通过复制,捕捉值类型的变量是复制值,捕捉引用类型的变量是通过复制指针,捕捉到的变量和指针在 block 内部的修改无法影响到 block 外部的值。可以在声明变量的时候加上 __block 标记。带有 __block 标记的变量在被捕捉后就可以被修改了。
问题的产生
Objective-C 是如何做到这一特性的呢?如果一个变量 foo 带有 __block 标记,并且被一个 block 捕捉,这时候在 block 内部修改 foo 的值会引起 block 外变量值的变化,不妨假设 block 创建了一个引用类型的变量指向了栈中的变量 foo 。不过想想都不可能,很明显这是不可靠地,因为程序运行过程中栈在不断变化,一旦 block 失去了当时栈的上下文,必然会产生野指针。
对推测的验证
所以我写了个 Demo ,用 block 捕获变量,来观察 block 是怎样操作的。
XCode Version 8.2.1 (8C1002) ARC Objective-C
#import "ViewController.h"typedef double (^SampleMultiplyBlockRef)();@interface ViewController ()@property (nonatomic, copy) SampleMultiplyBlockRef block;@end@implementation ViewController- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
__block double a = 10;
__block double b = 20;
// 变量初始化
NSLog(@"Var a after init: %f, %p", a, &a);
NSLog(@"Var b after init: %f, %p", b, &b);
a = 25;
a = 22;
// 修改变量值
NSLog(@"Var a after change: %f, %p", a, &a);
NSLog(@"Var b after change: %f, %p", b, &b);
SampleMultiplyBlockRef multiply = ^(){
// block 中修改变量值前
NSLog(@"Var a in block: %f, %p", a, &a);
NSLog(@"Var b in block: %f, %p", b, &b);
a = a + 1;
b = b + 1;
// block 中修改变量值后
NSLog(@"Var a after change: %f, %p", a, &a);
NSLog(@"Var b after change: %f, %p", b, &b);
return a * b;
};
// 变量被捕捉后,block执行前
NSLog(@"Var a out of block before execute: %f, %p", a, &a);
NSLog(@"Var b out of block before execute: %f, %p", b, &b);
// block 的地址
NSLog(@"mulitply %p", &multiply);
// block 的返回
NSLog(@"Where is %f", multiply());
// block 执行后
NSLog(@"Var a out of block: %f, %p", a, &a);
NSLog(@"Var b out of block: %f, %p", b, &b);
// 储存到属性中(注意:类型是 copy)
self.block = multiply;
NSLog(@"mulitply %p", &multiply);}- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.block();
// 作为属性储存的 block 地址
NSLog(@"mulitply %p", &_block);
}
@end
Demo 比较简单,就是在 viewDidLoad 方法内创建 block 并作为属性储存下来,在 viewDidAppear 方法中执行 block。
运行后我们一条一条的分析输出结果,主要关注值和内存地址:
首先在方法内部创建 a 和 b 两个值类型变量。
2017-01-13 09:24:17.648 Test Project[36246:1304073] Var a after init: 10.000000, 0x7fff5bbf9a482017-01-13 09:24:17.649 Test Project[36246:1304073] Var b after init: 20.000000, 0x7fff5bbf9a28
之后在方法内部修改 a 和 b 的值,可以看到两个变量的值发生了变化,但是内存地址并没有变化(废话)。
2017-01-13 09:24:17.649 Test Project[36246:1304073] Var a after change: 25.000000, 0x7fff5bbf9a482017-01-13 09:24:17.650 Test Project[36246:1304073] Var b after change: 22.000000, 0x7fff5bbf9a28
block 声明完成,此时发现两变量的地址都发生了变化,应该是已经完成变量的捕获。
2017-01-13 09:24:17.650 Test Project[36246:1304073] Var a out of block before execute: 25.000000, 0x60800002e2d82017-01-13 09:24:17.651 Test Project[36246:1304073] Var b out of block before execute: 22.000000, 0x60800002e338
block 在所在栈内执行过程中,对两变量赋值前的情况。
2017-01-13 09:24:17.652 Test Project[36246:1304073] Var a in block: 25.000000, 0x60800002e2d8
2017-01-13 09:24:17.652 Test Project[36246:1304073] Var b in block: 22.000000, 0x60800002e338
block 在所在栈内执行过程中,对两变量赋值后的情况。
2017-01-13 09:24:17.653 Test Project[36246:1304073] Var a after change: 26.000000, 0x60800002e2d8
2017-01-13 09:24:17.653 Test Project[36246:1304073] Var b after change: 23.000000, 0x60800002e338
block 在所在的栈内执行完成后,两变量的情况。
2017-01-13 09:24:17.654 Test Project[36246:1304073] Var a out of block: 26.000000, 0x60800002e2d8
2017-01-13 09:24:17.654 Test Project[36246:1304073] Var b out of block: 23.000000, 0x60800002e338
拷贝到堆中的 block 执行过程中,对两变量赋值前的情况。
2017-01-13 09:24:17.666 Test Project[36246:1304073] Var a in block: 26.000000, 0x60800002e2d8
2017-01-13 09:24:17.666 Test Project[36246:1304073] Var b in block: 23.000000, 0x60800002e338
拷贝到堆中的 block 执行过程中,对两变量赋值后的情况。
2017-01-13 09:24:17.667 Test Project[36246:1304073] Var a after change: 27.000000, 0x60800002e2d8
2017-01-13 09:24:17.667 Test Project[36246:1304073] Var b after change: 24.000000, 0x60800002e338
栈中刚声明后的 block 地址
2017-01-13 09:24:17.651 Test Project[36246:1304073] mulitply 0x7fff5bbf99f8
栈中执行后的 block 地址
2017-01-13 09:24:17.656 Test Project[36246:1304073] mulitply 0x7fff5bbf99f8
拷贝到堆中的 block 地址
2017-01-13 09:24:17.667 Test Project[36246:1304073] mulitply 0x7fac94d06338
结论
这样看下来,在 block 捕获后,变量的地址就发生了变化,变量的存活也不再依赖于栈,因此应该是储存在堆区。
block 复制到堆后,之前捕获的的变量地址依然没有变化,可以推断 block 中的变量值应该是指针指向了同一个地址。
关注微信订阅号:iOS开发笔记本
http://weixin.qq.com/r/Kji_plrEqwfUrR7F9204
关注知乎专栏:iOS开发学记笔记
https://zhuanlan.zhihu.com/iOSDevNote