本文的内容主要是基于Clang编译器的官方文档所写。
在开始探索Block的本质之前,大家先试着分析一下,下面的代码会输出什么:
void main() { |
如果你对输出结果不是那么有把握的话,那么相信通过今天的这篇文章,你会有一个明确的答案。
Clang
先说些题外话,什么是Clang?Clang是C++编写的编译器。我们知道,我们平常代码所写的任何程序,最终都需要通过编译器转换成与语言无关的机器二进制代码。而Clang,则是支持/C++/Objective-C/Objective-C++的编译器。那我们在做OC开发时,可能也会听说LLVM编译器,那么Clang和LLVM之间是什么关系呢?
它们的关系如下图所示:
LLVM架构
Clang是编译器的前端,它会分析具体的编程语言,然后用于生成与机器无关的中间代码。而LLVM是编译器的后端,与具体编程语言无关,而是会去分析统一的中间代码,生成符合对应机器的目标程序。
这样拆分前端后端的好处在于,前后端可以独立的替换,便于编译器的优化。
关于Clang,我们了解这些就足够了。
Block的本质
回到Block上来。我们在使用Block语法时,总会感觉到有些奇怪:
^{ |
这么一个^{}
是什么鬼?似乎在别的语言中也没有见过这么个关键字定义。其实,^{}
对于Clang编译器来说,仅仅是一个语言标记,它会告诉Clang,这里我需要定义一个Block类型的结构体。
而Clang发现这个语言标记时,会将^{}
这么一个奇怪的定义,转换为C语言中的结构体
。经过Clang转换后的Block,其形式是这样的:
struct Block_literal_1 { |
笔者将Block结构体定义分成了三个部分:
- Block基本信息以及 invoke函数指针
- Block descriptor指针
- Block所截取的外部变量
在这里我们得出结论:Block的本质是一个C语言的struct。
Block对应的结构体
上面探讨了Block的本质是一个struct,接下来我们就来详细看一下这个 Block struct的定义。
Block基本信息以及Block descriptor
struct Block_literal_1 { |
我们先来看Block struct的第一部分和第二部分。至于Block的第三部分,外部变量的截取,我们会在下面单独的章节进行讨论。
当我们声明一个Block时,对应的Block struct会被如下初始化:
系统会声明并初始化一个Block descriptor结构体。初始化Block descriptor步骤如下
a. Block descriptor 的size部分会被设置为Block结构体的大小
b. copy_helper 和 dispose_helper函数指针会被设置为对应的函数指针(如果需要这两个helper函数的话)系统初始化Block 结构体。 初始化Block 结构体的步骤如下:
a. isa部分会被设置为__NSGlobalBlock__
/__NSMallocBlock__
/__NSStackBlock__
所对应的地址
。注意这里是地址,而不是NSMallocBlock这些变量。
b. flags 会被置为对应的flag数值。比如,如果Block struct需要copy,dispose helper函数时,响应的flag会被置位。同时,flags还有标志Block ABI版本的功能。
c. 设置invoke函数指针指向对应的函数。该函数的第一个参数是Block struct本身的指针,而其余的参数则是Block执行时外部要传入的参数(如果有的话)
举个例子,对于下面的Block:
^{ |
Clang会创建如下内容:
struct __block_literal_1 { |
那么Block struct将会如下被初始化:
struct __block_literal_1 _block_literal = { |
这是Clang文档给出的官方例子,但是我们这里不要去纠结flags究竟是设置的什么,因为根据本人的测试,其flags的值并不是1<<29。
这里有个问题,就是什么时候isa会被设为&__NSGlobalBlock__
/&__NSMallocBlock__
/&__NSStackBlock__
呢?
- 当Block中没有引用外部变量,或引用了全局变量,const 标量或static变量时,Block的isa会被设置为
&__NSGlobalBlock__
。这时的Block生命周期是伴随程序始终的。 &__NSStackBlock__
表示这个block,是在栈上面分配的,出了栈就会消亡。使用了外部栈变量,就会是__NSStackBlock__
类型。&__NSMallocBlock__
表示Block复制到堆上面了,可以存储下来,以后使用。当Block引用了外部的OC对象,Block对象或用__block修饰的变量时,Block会被设置为&__NSMallocBlock__
类型。这里有一点要注意,在ARC的情况下。只要将block赋值给变量,就自动帮你复制了。也就是说,如果将一个栈上的block赋值给另一个block变量,则被赋值的block变量类型是 &__NSMallocBlock__ 类型。
如下面代码:
int a = 13; |
输出为:
而对于const类型的引用,
const int a = 13; // 这里是const引用 |
输出为:
这是因为对于Global,不必需要再在堆上开辟一块内存。
Block的外部变量截取
理解Block的关键,在于理解Block是如何处理外部变量的。
我们先来想一想,Block中会截取那些类型的外部变量:
- 全局/静态变量
- 自动(auto)存储类型
- Block类型
- NSObject类型
- __block修饰的变量
截取全局/静态类型变量
对于全局/静态变量,Block会直接引用这类变量,不会copy。 例如,
static int a = 13;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"Outside Block, static int a address is %p", &a);
^{
NSLog(@"Inside Block, static int a address is %p", &a);
}();
}
输出为:
在Block 外和Block内,static int a的地址是一样的,Block并没有做特殊的处理。
截取自动存储类型变量
所谓自动存储类型,指的是auto类型
。我们可以理解为栈上的变量(Block类型、__block、NSObject类型除外),其内存会有系统自动释放。
对于auto类型
的变量截取,Clang文档有如下描述:
Variables of auto storage class are imported as const copies.
也就是说,auto类型会在Block中用const
copy一份。也就是说Block内、外是完全不同的两个变量。
我们做个测试:
int b = 12; |
输出为:
可以看到,在Block外和Block内部,表面上同样的b变量,其地址是不一样的。究其原因,就是因为在Block内部,系统会默默的const copy一份b。
这时候,Block的数据结构是这样的:
int x = 10; |
一般的,对于标量类型(int, float, bool等基本类型),struct,unions和函数指针类型,都会采用const copy的方式,将Block外部的变量拷贝到Block内部。
这里需要注意一点,在iOS系统中,当我们把一个stack
上的Block赋值给一个Block变量时:
void (^vv)(void) = ^{ printf("x is %d", x); } |
会默认调用Block的copy方法,即,上面实际上是如下代码:
void (^vv)(void) = [^{ printf("x is %d", x); } copy]; |
这样得到的vv,是一个在堆上的Block变量。这时候再输出vv中x的地址,会得到一个堆上的地址。
因此,我们在做实验的时候,不要输出对拷贝后的Block中变量地址,而应该直接输出Block中的地址:
^{ |
上面代码中并没有赋值,因此会输出栈上的a的const copy地址。
截取Block类型变量
对于截取Block类型的变量,在Block内部,会保留const copy其Block指针。
如下代码:
int a4 = 13; |
输出为:
这里可以看到,对于Block变量,existingBlock(注意,这个existingBlock变量是一个Block指针,而不是Block本身)被const copy了一份到Block中。而对于Block指针所指向的Block实体,并没有发生改变。
也就说,在Block内部和外部,会有两个Block指针,指向了同一个Block结构体。
这里再次强调一下,我们所声明的Block变量existingBlock,是一个指向Block类型的指针,而不是Block实体。正如同NSObject *obj = [NSObject new]一样,obj是一个指向NSObject的指针,而不是NSObject实体
。
下面是Clang文档的例子:
void (^existingBlock)(void) = ...; |
这时候Block的数据结构是:
struct __block_literal_4 _block_literal = { |
截取NSObject类型变量
在Clang中,NSObject类型变量被当做__attribute__((NSObject))
类型。Block截取NSObject对象时,同样会做一份const copy NSObject *
。
比如:
@interface MyObject : NSObject |
输出为:
可以看到,当Block对NSObject做const copy时,仅是做了浅拷贝
,并没有复制指针所指向的内容,仅仅是const copy了指针。因此,这里的self指针地址是改变了,而self指针所指向的地址都是同一个。
就像上面Block类型变量的例子,是同一个道理。
而对于NSObject类型,同样需要两个copy helper函数:
void __block_copy_foo(struct __block_literal_5 *dst, struct __block_literal_5 *src) { |
截取__block修饰的变量
鉴于我们上面所说的都是const copy,因此对于在Block中对于其截取变量的任何改变,都是不被允许的。如果我们要修改Block内部的值,编译器就会提示如下错误:
那如何在Block中修改截取变量的值呢?我们自然会想到对外部变量加上__block
修饰符。我们将上面代码改成下面的形式,则会顺利编译通过:
__block int b = 13; |
输出为:
这里会发现一个有意思的现象,虽然在进入Block前后,b的地址并不一样!也就是在进入Block前后,其实会有两个不同的b。
之所以会这样,与Clang对于__block类型变量的处理有关。
当变量被标记为__block类型时,Clang会对变量b进行改写成一个如下格式的struct:
struct _block_byref_foo { |
比如:
int __block i = 10; |
会被Clang改写做:
struct _block_byref_i { |
可以看到,int __block i
被改写为了struct _block_byref_i
结构体。这里需要明确一点:
添加了__block关键字后的int b,实质类型并不是int类型,而是一个struct _block的结构体类型了。
这里有个关键的属性变量,forwarding
,forwarding
指向一个__block结构体。
当__block在栈上时,forwarding
会指向__block自身。而当__block在堆上生成一份copy时,这时候栈上的forwarding
会指向堆上的那一份拷贝。而在堆上的那个__block的__forwarding指针,则指向自己的首地址。
也就是说,只要通过forwarding
来操作__block结构体捕获的外部变量,实质上是操作的同一个变量。
我们用图片可以更清楚的弄懂其中的原理:
这也就是为什么,即使Block外和Block内部b
分别是两个变量,而b
的值却可以被改变的原因。因为在栈上的__block结构体
中,通过forwarding指针指向了堆上的Block的地址。那么当在Block内部修改b的值,也就是改变堆上的int b的值的时候,在Block外部再访问b的值的时候,其实在栈上的__block int b通过__forwarding 指针,访问到了堆上的__block int b,这让我们感觉在栈上的变量也被修改了。
这也就是为什么,在测试代码中,在执行完Block后,再输出b的地址,发现是和Block内部的地址一致,而不是进入Block之前的地址的原因。(以为进入Block后,再次访问b,实际上会指向堆上的那个b,而不是之前栈上的那个b)
当我们将__block的变量导入Block中时,Clang会作如下改写:
例如,
int __block i = 2;
functioncall(^{ i = 10; });
会被Clang做如下改写:
struct _block_byref_i {
void *isa; // set to NULL
struct _block_byref_voidBlock *forwarding;
int flags; //refcount;
int size;
void (*byref_keep)(struct _block_byref_i *dst, struct _block_byref_i *src);
void (*byref_dispose)(struct _block_byref_i *);
int captured_i;
};
struct __block_literal_5 {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal_5 *);
struct __block_descriptor_5 *descriptor;
struct _block_byref_i *i_holder;
};
void __block_invoke_5(struct __block_literal_5 *_block) {
_block_byref_i * i_holder = _block->i_holder;
i_holder->forwarding->captured_i = 10;
}
void __block_copy_5(struct __block_literal_5 *dst, struct __block_literal_5 *src) {
_Block_object_assign(&dst->i_holder, src->i_holder, BLOCK_FIELD_IS_BYREF | BLOCK_BYREF_CALLER);
}
void __block_dispose_5(struct __block_literal_5 *src) {
_Block_object_dispose(src->i_holder, BLOCK_FIELD_IS_BYREF | BLOCK_BYREF_CALLER);
}
static struct __block_descriptor_5 {
unsigned long int reserved;
unsigned long int Block_size;
void (*copy_helper)(struct __block_literal_5 *dst, struct __block_literal_5 *src);
void (*dispose_helper)(struct __block_literal_5 *);
} __block_descriptor_5 = { 0, sizeof(struct __block_literal_5) __block_copy_5, __block_dispose_5 };
上面的数据结构会做如下初始化
struct _block_byref_i i_holder = {( .isa=NULL, .forwarding=&i, .flags=0, .size=sizeof(struct _block_byref_i), .captured_i=2 )};
struct __block_literal_5 _block_literal = {
&_NSConcreteStackBlock,
(1<<25)|(1<<29), <uninitialized>,
__block_invoke_5,
&__block_descriptor_5,
& i_holder,
};
是否只有__block类型才能够在Block中被修改?
这里插入一个小测试,对于静态变量a,是否可以在Block中作出改变呢?
static int a = 13;
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"Outside Block, static int a address is %p", &a);
^{
NSLog(@"Inside Block, static int a address is %p", &a);
a++;
}();
NSLog(@"Now a is %d", a);
}
答案是可以的
,在Block之后,a的值变为14。这是因为对于全局/静态变量
而言,Block会直接引用变量,而不会做const
copy。
所以,我们这一节讨论的,是除去全局、静态变量外,被Block const
copy的其他的类型变量。
小测试
题目一. 下面代码会输出什么?
typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int i = 13;
blockType blk = ^{
NSLog(@"In block i = %d", i);
};
i += 2;
blk();
NSLog(@"Now i = %d", i);
}
return 0;
}
这里考察对于auto类型变量,Block的截取方式。因为auto变量会在Block中做一份const
copy,因此在Block内外,实质上应该存在两个i
。
这里的输出为:
image
题目二. 下面的代码会 正常输出/编译错误/runtime crash
typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *str = @"Hello";
blockType blk = ^{
str = @"World";
};
blk();
NSLog(@"Now str is %@", str);
}
return 0;
}
因为对于NSObject类型,在Block中会当做NSObject *const
obj处理,此时是一个指针常量。对于指针常量,是不能够更改其指针所指向的位置的,因此,这里会出现编译错误。
题目三. 下面的代码会 正常输出/编译错误/runtime crash
typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSString *str = @"Hello";
blockType blk = ^{
str = @"World";
};
blk();
NSLog(@"Now str is %@", str);
}
return 0;
}
因为str变量用了__block
修饰,因此__block NSString *str
实质上一个__block struct 类型变量:
struct _block_byref_str {
void *isa;
struct _block_byref_str *forwarding;
int flags;
int size;
NSString *captureStr;
}
当创建__block 类型变量时,在Block结构体中,会存储__block结构体指针:
struct __block_literal {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal * _cself);
struct __block_descriptor *descriptor;
struct _block_byref_str *str_holder; // __block结构体指针
}
当调用invoke方法时,会是这样的:
void invoke(struct __block_literal * _cself) {
_block_byref_str *str_holder = _cself->str_holder;
str_holder->forwarding->captureStr = @"World";
}
由于通过forwarding指针,确保了Block外部和内部的str都是一个指针,因此,当Block内部的str指向新的地址时(str = @"World"
),在Block外部的str也指向了新的地址。(因为它们是同一个东西)。
这个过程用图表示为:
- __block str = @”World”;
<img src="//upload-images.jianshu.io/upload_images/4824593-597e4d8dd456a82b" width="4032" height="3024" />
image
- 当在Block中操作str=@”World”时,相应的__block结构体会拷贝到heap上,同时,stack上的__block结构体的forwarding指针也会指向heap上的那份copy:
<img src="//upload-images.jianshu.io/upload_images/4824593-3501c452828341e9" width="4032" height="3024" />
image
- 因此,在Block外面再次输出str的内容时,由于这时候stack上__block结构体的forwarding指针已经指向了heap上的__block结构体,因此也会输出heap上的captured_str指针所指向的内容:
@“World”
。
为了验证我们的猜测,我们可以用如下代码:
image
在进入Block前,Block中,进入Block后分别设置断点,并打印aR
指针的地址&aR
,会得到如下结果:
image
可以看到,在Block中和进入Block后,aR
的地址是一样的,而在进入Block之前,则是另一个地址。这是因为在stack上的__block结构变量,将其forwarding指针指向了heap地址所导致的。
题目四. 下面的代码会 正常输出/编译错误/runtime crash
typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *aStr = @"Hello";
__block NSString *str = aStr;
blockType blk = ^{
str = @"World";
};
blk();
NSLog(@"Now a aStr is %@", aStr);
NSLog(@"Now str is %@", str);
}
return 0;
}
这个题目和题目三类似,只不过对于str的赋值由__block NSString *str = @"Hello"
变成了__block NSString *str = aStr
。
上面这段代码会正常输出,其结果为:
image
至于str为什么会由@”Hello”变成@“World”,其原因见题目三。
这里aStr是没有任何变化的,这是因为在将str在Block中赋值为@”World”时,仅仅是将str指向了新的地址,而没有更改原地址的内容。而aStr一直指向旧的地址,也就是值为@”World”的地址。
题目五. 下面的代码会 正常输出/编译错误/runtime crash
NSMutableString *str = [NSMutableString stringWithString:@"Hello"];
blockType blk = ^{
[str appendString:@" World"];
};
blk();
NSLog(@"Now str is %@", str);
答案是会正常输出。因为对于NSObject类型来说,Block会copy一份指针常量来保存NSObject的地址。所谓指针常量,是指指针指向的地址是不可用更改的。而这里在Block中,并没有更改指针指向的地址,而仅仅是改变了指针指向地址中的值,这个操作是允许的。
其输出结果为:
image
同样的,类似还有下面代码,也是可以正常运行,并输出名字Tim:
MyRetaion *aR = [MyRetaion new];
aR.name = @"Jack";
blockType blk = ^{
aR.name = @"Tim";
};
blk();
NSLog(@"Now name is %@", aR.name);
总结
在本篇文章中,我们根据Clang的官方文档,分析总结了Clang为了支持Block,其背后所使用的数据结构。同时,我们重点分析了Block对于不同类型的外部变量的截取方式。按照Block不同的处理方式,Block截取的变量类型可以分为:
- 全局/静态类型
- auto类型
- Block类型
- NSObject类型
- __block类型
不同的类型,Block都有不同的截取处理方式。
通过深入了解Block的机制,相信对大家编程中正确高效的使用Block,是很有帮助的。
现在来回答我们文章最开始的部分,代码的输出结果为:
a = 14, b = 13, str = HelloWorld
至于原因,相信大家都会知道了:)