Objective-C 中的 Block (1)

block 是 Objective-C 中的重要技术。本文主要是写了一点对 block 应用的探究。

什么是 Block

首先补充几个概念和解释帮助理解,大神们直接略过:

  1. block 本质是一个结构体。
  2. 结构体可以创建在栈上,堆上,还可以在全局区,因此就有对应的三种 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

你可能感兴趣的:(Objective-C 中的 Block (1))