Objective-C 中的 Blocks 原理探究

前言

Blocks 是 OS X v10.6 和 iOS 4.0 引入的新特性。觉的自己对 Blocks 掌握的还不错的话,那就来试着做做这五个测试题 。怎么样,全做对了吗?没全做对的话就静下心来,好好的总结一下吧。

本文章的目录结构:

1 Blocks 基础知识    
    1.1 Blocks 是什么
    1.2 Block 变量
    1.3 __block 变量
2 Block 使用要注意的问题    
    2.1 Block 的存储域
    2.2 __block 变量会随着 Block 变化而变化
    2.3 解决 Block 循环引用
3 Block 原理探究    
    3.1 简单的 Block 原理
    3.2 Block 截获变量的原理
    3.3 Block 截获局部变量的出错的原因
    3.4 Block 截获静态变量的原理
    3.5 Block 截获 __block 变量的原理
4 参考文章

1 Blocks 基础知识

1.1 Blocks 是什么

在开发 iOS 程序时经常会用到 Blocks ,但它究竟是什么呢?苹果官方文档里给的解释是这样的:

Blocks are Objective-C objects, which means they can be added to collections like NSArray or NSDictionary. They also have the ability to capture values from the enclosing scope, making them similar to closures or lambdas in other programming languages.
Blocks 是 Objective-C 的对象(稍后会介绍它是什么类的对象),这意味着它们可以添加到像 NSArray 或 NSDictionary 这样的集合中。他们也有捕获词法范围内变量的能力,使他们和其他编程语言里的闭包或者 lambda 相似。

上面是官方给出的定义。其实我们可以用一句话来总结: 能捕获局部变量的匿名函数 。首先说说它为什么是函数,然后在说说它是怎么捕获局部变量的。我们现在来看看 C 语言中定义函数的语法:

void test (void) {
    printf("Hello World!\n");
}

再来看看 Block 的语法:

^ void (void) {
       NSLog(@"Hello World");
};    

//其实上面的代码可以简写成下面这样,这里为了说明它和 C 函数的联系:
^ {
    NSLog(@"Hello World");
};

Block 和 C 函数相比,只有以下两点不同:

没有函数名字
多了 " ^ " 符号。 

通过上面的比较我们 Block 为什么是匿名函数了,在说明它怎么捕获局部变量。

NSInteger var = 100;
oid (^blk)(void) = ^{
    NSLog(@"%ld",var);
};
var = 0;
blk();        //执行后输出100

上面代码中的 Block 语法捕获了局部变量 var 的值,只是变量瞬间的值,并不会修改 val 变量的值。加上 __block 说明符后就能修改了,这部分在 1.3 里面介绍。我们明白了 Blocks 就是 能捕获局部变量的匿名函数

1.2 Block 变量

在 C 语言中可以把函数赋值给函数指针类型的变量。如下所示:

int test (int var) {
    return var + 1;
}

int (*testPtr) = &test;

同理,在 Block 语法下也可以将 Block 语法赋值给 Block 类型的变量中。我们先看一下 Block 语法。

^ 返回值类型 参数列表 表达式

//返回值类型和参数列表为空的时候我们可以省略      

Block 类型变量和 C 语言函数指针基本一样,只是把 * 替换成了 ^ 。如下所示:

int (^blk)(int);

下面把 Block 语法赋值到 Block 类型的变量中,如下所示:

int (^blk)(int) = ^(int count) {
    return count + 1;
};

上面使用的 Block 类型变量表示太复杂,我们可以用 typedef 来重新定义,如下所示:

typedef int (^block)(int);

以后我们就可以使用 block 来定义 Block 类型的变量了。
Block 类型的变量可以有以下用途:

* 局部变量(也叫自动变量)
* 全局变量
* 静态变量
* 静态全局变量
* 函数参数
* 函数返回值        

1.3 block 变量
我们把赋有 `
block修饰符的变量称为block` 变量。加上 block 后就可以修改 Block 语法块中捕获的变量了。

__block NSInteger var = 100;
void (^blk)(void) = ^{
    var = 200;
};
blk();
NSLog(@"%ld",var);    //输出结果为 200

如果不加 __block 修饰符,那么编译是不会通过的,如下所示:

NSInteger var = 100;
void (^blk)(void) = ^{
    var = 200;
};
blk();
NSLog(@"%ld",var);

会得到一个编译错误提示:

block_error

2 Block 使用要注意的问题

2.1 Block 的存储域
官方文档上说 Blocks 是 Objective-C 的对象,那它到底是是哪个类的对象呢?通过下面的代码来测试下:

// 代码是在 ARC 下测试的
void (^maxBlk)(void) = ^(void){};

- (void)test{
    NSInteger var = 1024;
    void (^blk)(void) = ^{ printf("%ld\n", var); };
    blk();
    NSString *str3 = @"1234";
    NSLog(@"\n堆:%@\n 栈:%@\n 全局:%@\n",blk,^{NSLog(@":%@", str3);},maxBlk);
}

//输出的结果为:
堆:<__NSMallocBlock__: 0x7fd2ca5218d0>
栈:<__NSStackBlock__: 0x7fff529b4b98>
全局:<__NSGlobalBlock__: 0x10d24e120>    

到现在为止知道了 Block 是 __NSMallocBlock__ 或者 __NSStackBlock__ 或者 __NSGlobalBlock__ 类的对象。通过它们的名字,可以知道它们对应的 Block 分别存放在 栈``数据区(全局) 中。在 ARC 下, __NSStackBlock__ 不是很常见,用的也很少。那么问题又来了,Block 什么时候存放在堆上?又什么时候放在全局区呢?在总结出来的情况如下:

放在堆上的情况:

* 将 Block 赋值给用 __strong 修饰符修饰的 id 类型的类或 Block 类型成员变量时
* Block 作为函数的返回值时
* 在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时
* 调用 Block 的 copy 实例方法时

放全局上的情况:

* 放全局变量的地方有 Block 语法时
* Block 语法中的表达式中不使用捕获的变量时

2.2 __block 变量会随着 Block 变化而变化
当 Block 被复制到堆的时候,它所使用的 __block 变量也会被复制到堆上,并且被 Block 持有。

当 Block 被废弃的时候, 它持有的 `__block` 变量也就会被释放。

当 Block 已经在堆上的时候,复制 Block 对它使用的 `__block` 变量没任何影响。

当多个 Block 同时使用 `__block` 变量时,其中任何一个 Block 被复制到堆上的时候, `__block` 变量也会被复制到堆上,并且被 Block 持有,当剩下的 Block 被复制到堆上时,被复制的 Block 持有 `__block` 变量,并且增加 `__block` 的引用计数。

2.3 解决 Block 循环引用

先来看看下面的列子:

block_retain_cycle1.jpg

使用 ZWObject 这个类:

ZWObject *A = [[ZWObject alloc] init];
NSLog(@"A:%@",A);

通过苹果官方文档的 Encapsulating Data
给出的下面的这段话,我们明白:默认情况下,Objective-C 类的对象对它的属性和变量是强引用的关系:

By default, both Objective-C properties and variables maintain strong references to their objects.

通过上面我们可以知道,此例子中 ZWObject 类的对象 A 对它的成员变量 _blk 是强引用关系。在把 Block 赋值给 _blk 的时候, _blk 对 Block 是强引用。也就是说 A 对象对 Block 是强引用关系。并且由于 Block 语法赋值给了 _blk 变量,此时生成在栈上的 Block 便会由栈复制到堆,并且持有它所使用的 self,也就是持有对象 A。此时 A 对象持有 Block,Block 持有 A 对象,循环引用就发生了。编译器在编译的时候就能查出,所以给出如 2.3 中图所示的警告。

解决此循环引用有两种办法:一是使用 __weak 修饰符,二是使用 __block 变量。

使用 __weak修饰符:在 ZWObject 的 init 方法中做如下修改:

- (instancetype)init {
        self = [super init];
        __weak typeof(self) weakSelf = self;
        _blk = ^{
               NSLog(@"%@",weakSelf);
        };        
           return self;
}

使用 __block :有两点需要注意

在 Block 里面把使用后的 `__block` 变量设置为空。
必须执行 Block 方法。

在 ZWObject 的 init 方法中修改如下:

- (instancetype)init {
    self = [super init];
    __block typeof(self) bSelf = self;
    _blk = ^{
        NSLog(@"%@",bSelf);
        bSelf = nil;
    };
    return self;
}

//必须要执行此方法
- (void)execBlock {
    _blk();
}

在使用 ZWObject 的地方修改如下:

ZWObject *A = [[ZWObject alloc] init];
[A execBlock];
NSLog(@"object:%@",A);

3 Block 原理探究

3.1 简单的 Block 原理
Block 实际上就是被作为普通的 C 语言来来处理的,可以通过 clang (LLVM) 编译器转换成我们能读懂的代码。通过 -rewirte-ojbc 选项就能把含有 Block 语法的源代码转换为 C++ 的源代码。下面我们来看一下具体怎么操作。
源码如下:

block0_test.jpg

调用 clang -rewrite-objc block0.c 命令后会生成一个 block0.cpp ,打开这个文件,发现里面的代码居然有 500 多行,也是醉了。。。 抽出其中的主要的代码如下:

block_cpp.jpg

根据变换后的源码我们可以看出来,clang 会根据 Block 所属的函数名(此处为 main )和该 Block 在函数中出现的顺序(此处为 0 )来给函数命名。

都有 printf("Hello, World!"); 这句的对应关系如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Hello, World!");
}

通过源码和转换后的源码进行比较,能看出源代码中的 Block 语法赋值给 blk 对应的为以下代码:

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

源代码中调用 Block 的地方对应如下代码:

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

现在来仔细分析一下:

先来看一下 Block 语法赋值给 blk 对应的转换后的代码,对应的类型转换太多,去掉类型转换后,如下所示:

//转换后的代码
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

//去掉类型转换后的代码
void (*blk)(void) = &__main_block_impl_0(参数1, 参数2);

可以看出来,Block 语法赋值给 blk 的过程就想当于把 __main_block_impl_0 结构体指针赋值给函数指针 blk 的过程。

参数 1 为 __main_block_func_0,也就是匿名函数的函数名称,通过__main_block_impl_0 结构体的构造函数可以知道把它赋值给了 FuncPtr 。参数 2 为 __main_block_desc_0_DATA,其实就是 __main_block_impl_0 的结构体的大小。

再来看看调用的部分:

//转换后的代码
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

//去掉类型转换后的代码
(*blk->impl.FuncPtr)(blk);

通过分析可知,其实就是函数指针调用函数的过程。从这个过程中可以得出 Block 以自身作为参数进行了传递,__main_block_func_0 函数的参数 __cself 指向了 Block 自己。通过 __main_block_impl_0 结构体构造函数里的这行代码 impl.isa = &_NSConcreteStackBlock,也证实了此处的 Block 是 _NSConcreteStackBlock “类” 的对象。 到此为止我们终于明白了 Block 的实质。

3.2 Block 截获变量的原理
同上面的分析方法一样,先来看一下源代码:

block1_test.jgp

经 clang 转换后的源码如下:

block1_test.jgp

同 3.1 转换后的源码比较,被 Block 捕获的 val 变量被添加到 __main_block_impl_0 的结构体中。通过这一点我们明白了在执行 Block 之前修改被它捕获的变量不起作用的原因。

3.3 Block 截获局部变量的出错的原因

同上面一样,先来看源码:

block2_test.jpg

用 clang 编译时报错,提示信息如下:

block2_error.jpg

经 clang 编译时候报错,提示我们变量是不能修改的,并提示缺少了 __block 修饰符。那它为什么不能被修改呢?此处的 var 变量为局部变量,在 OBJective-C 中我们经常把 Block 用来处理回调,执行 Block 的时候 var 可能已经被回收了。当超出 var 的作用域后,var 变量被回收,如果在 Block 里面修改 var 的话就崩溃了。这里不让在 Block 里面修改 var 的值是非常合理的。

3.4 Block 截获静态变量的原理
同上面一样,先来看源码:

block3_test.jpg

经 clang 编译后的源码如下所示:

block3_cpp.jpg

__main_block_impl_0 结构体的构造函数中,是把 var 的地址传递过去的。在 __main_block_func_0 使用 var 时用的是 var 的地址。为什么静态变量能够在 Block 内修改呢?这是因为静态变量存储在数据区(data 区),它能保证无论什么时候执行 Block ,它都没被回收,所以能在 Block 内部修改。

3.5 Block 截获 __block 变量的原理

再来看最后一种情况,还是先看源码:

block4_test.jpg

经 clang 编译后的源码如下所示:

block4_test.jpg

加上 __block 后代码量增大了。同 Block 捕获静态变量相比,多出了以下内容:

* var 变量变成为了 `__Block_byref_val_0` 结构体类型的变量
* __main_block_desc_0 结构体中多出了 copy 和 dispose 方法。
* __main_block_impl_0 结构体中多出了 `__Block_byref_val_0` 结构体类型的指针
* __main_block_func_0 在访问 `__block` 变量的时候是通过 `__forwarding` 指针来访问的

那么一大堆问题就来了。

__main_block_impl_0 结构体中为什么不把 __Block_byref_val_0 结构体引入进来,而是引用的 __Block_byref_val_0 结构体类型的指针?

__main_block_desc_0 结构体里多出来的 copy 和 dispose 方法有什么用呢?

访问 __block 变量的时候为什么要通过 __forwarding 指针来访问? 

我们从中发现 __block 变量竟然被转成了 __Block_byref_val_0 结构体类型的变量。__Block_byref_val_0 结构体申明如下:

struct __Block_byref_val_0 {
      void *__isa;
    __Block_byref_val_0 *__forwarding;
    int __flags;
    int __size;
    int val;
};

该结构体中的最后一个成员变量相当于原来的 __block 变量。__forwarding 是指向自己的结构体指针。__main_block_impl_0 结构体中引入的是 __Block_byref_val_0 结构体指针,这样能保证从多个 Block 中使用的变量为同一个 __block 变量,也能保证一个 Block 使用多个 __block 变量时,只要增加 __main_block_impl_0 结构体的成员变量和构造函数的参数即可。当 Block 被复制到堆的时候,它所使用的 __block 变量也会被复制到堆上,并且被 Block 持有。__block 被复制到堆上时正是用的 __main_block_desc_0 结构体里的 copy 函数。当 Block 被销毁的时候,它使用的 __block 变量也需要被销毁,此时用的正是 __main_block_desc_0 结构体里的 dispose 函数。不管 Block 在栈上,还是在堆上,要想正确的访问它使用的 __block 变量,用 __block 结构体里指向自己的 __forwarding 是最好的办法。

4 参考文章