本文的内容主要是基于Clang编译器的官方文档所写。
在开始探索Block的本质之前,大家先试着分析一下,下面的代码会输出什么:
void main() {
__block int a = 13;
int b = 13;
NSMutableString *str = [[NSMutableString alloc] initWithString:@"Hello"];
void(^blockTest)(void) = ^{
NSLog(@"a = %d, b = %d, str = %@", a, b, str);
};
a++;
b++;
[str appendString:@"World"];
blockTest();
}
如果你对输出结果不是那么有把握的话,那么相信通过今天的这篇文章,你会有一个明确的答案(答案在文章最后)。
Clang
先说些题外话,什么是Clang?Clang是C++编写的编译器。我们知道,我们平常代码所写的任何程序,最终都需要通过编译器转换成与语言无关的机器二进制代码。而Clang,则是支持/C++/Objective-C/Objective-C++的编译器。那我们在做OC开发时,可能也会听说LLVM编译器,那么Clang和LLVM之间是什么关系呢?
它们的关系如下图所示:
Clang是编译器的前端,它会分析具体的编程语言,然后用于生成与机器无关的中间代码。而LLVM是编译器的后端,与具体编程语言无关,而是会去分析统一的中间代码,生成符合对应机器的目标程序。
这样拆分前端后端的好处在于,前后端可以独立的替换,便于编译器的优化。
关于Clang,我们了解这些就足够了。
Block的本质
回到Block上来。我们在使用Block语法时,总会感觉到有些奇怪:
^{
NSLog(@"Hello");
};
这么一个^{}
是什么鬼?似乎在别的语言中也没有见过这么个关键字定义。其实,^{}
对于Clang编译器来说,仅仅是一个语言标记,它会告诉Clang,这里我需要定义一个Block类型的结构体。
而Clang发现这个语言标记时,会将^{}
这么一个奇怪的定义,转换为C语言中的结构体
。经过Clang转换后的Block,其形式是这样的:
struct Block_literal_1 {
// 第一部分. Block基本信息以及 invoke函数指针
void *isa; // initialized to &__NSGlobalBlock__ or &__NSMallocBlock__ or &__NSStackBlock__
int flags;
int reserved;
void (*invoke)(void *, ...);
// 第二部分. Block descriptor指针
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
void (*dispose_helper)(void *src); // IFF (1<<25)
// required ABI.2010.3.16
const char *signature; // IFF (1<<30)
} *descriptor;
// 第三部分. Block所截取的外部变量(如果有的话)
// imported variables
};
笔者将Block结构体定义分成了三个部分:
- Block基本信息以及 invoke函数指针
- Block descriptor指针
- Block所截取的外部变量
在这里我们得出结论:Block的本质是一个C语言的struct
。
Block对应的结构体
上面探讨了Block的本质是一个struct,接下来我们就来详细看一下这个 Block struct的定义。
Block基本信息以及Block descriptor
struct Block_literal_1 {
// 第一部分. Block基本信息以及 invoke函数指针
void *isa; // initialized to &__NSGlobalBlock__ or &__NSMallocBlock__ or &__NSStackBlock__
int flags;
int reserved;
void (*invoke)(void *, ...);
// 第二部分. Block descriptor指针
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
void (*dispose_helper)(void *src); // IFF (1<<25)
// required ABI.2010.3.16
const char *signature; // IFF (1<<30)
} *descriptor;
...
};
我们先来看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:
^ { printf("hello world\n"); }
Clang会创建如下内容:
struct __block_literal_1 {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal_1 *);
struct __block_descriptor_1 *descriptor;
};
void __block_invoke_1(struct __block_literal_1 *_block) {
printf("hello world\n");
}
static struct __block_descriptor_1 {
unsigned long int reserved;
unsigned long int Block_size;
} __block_descriptor_1 = { 0, sizeof(struct __block_literal_1) };
那么Block struct将会如下被初始化:
struct __block_literal_1 _block_literal = {
&__NSGlobalBlock__,
(1<<29), ,
__block_invoke_1,
&__block_descriptor_1
};
这是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;
NSLog(@"block type is %@", NSStringFromClass([^{NSLog(@"%d", a);} class]));
blockType1 blk2 = ^{
NSLog(@"%d", a);
};
NSLog(@"block type is %@", NSStringFromClass([blk2 class]));
输出为:
而对于const类型的引用,
const int a = 13; // 这里是const引用
NSLog(@"block type is %@", NSStringFromClass([^{NSLog(@"%d", a);} class]));
blockType1 blk2 = ^{
NSLog(@"%d", a);
};
NSLog(@"block type is %@", NSStringFromClass([blk2 class]));
输出为:
这是因为对于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;
NSLog(@"Outside Block, address of int b is %p", &b);
^{
NSLog(@"Inside Block, address of int b is %p", &b);
}();
输出为:
可以看到,在Block外和Block内部,表面上同样的b变量,其地址是不一样的。究其原因,就是因为在Block内部,系统会默默的const copy一份b。
这时候,Block的数据结构是这样的:
int x = 10;
void (^vv)(void) = ^{ printf("x is %d\n", x); }
x = 11;
vv();
struct __block_literal_2 {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal_2 *);
struct __block_descriptor_2 *descriptor;
const int x; // 这里会有一份const copy
};
struct __block_literal_2 __block_literal_2 = {
&_NSConcreteStackBlock,
(1<<29), ,
__block_invoke_2,
&__block_descriptor_2,
x
};
一般的,对于标量类型(int, float, bool等基本类型),struct,unions和函数指针类型,都会采用const copy的方式,将Block外部的变量拷贝到Block内部
。
这里需要注意一点,在iOS系统中,当我们把一个stack 上的Block赋值给一个Block变量时:
void (^vv)(void) = ^{ printf("x is %d\n", x); }
会默认调用Block的copy方法,即,上面实际上是如下代码:
void (^vv)(void) = [^{ printf("x is %d\n", x); } copy];
这样得到的vv,是一个在堆上的Block变量。这时候再输出vv中x的地址,会得到一个堆上的地址。
因此,我们在做实验的时候,不要输出对拷贝后的Block中变量地址,而应该直接输出Block中的地址:
^{
NSLog(@"Inside Block, static int a address is %p", &a);
}();
上面代码中并没有赋值,因此会输出栈上的a的const copy地址。
截取Block类型变量
对于截取Block类型的变量,在Block内部,会保留const copy其Block指针。
如下代码:
int a4 = 13;
void (^existingBlock)(void) = ^{NSLog(@"Hello %d", a4);};
NSLog(@"Outside Block, address of block pointer address is %p, block address is %p", &existingBlock, existingBlock);
^{
NSLog(@"Inside Block, address of block pointer address is %p, block address is %p", &existingBlock, existingBlock);}();
blockType1 blk = existingBlock;
blk();
输出为:
这里可以看到,对于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) = ...;
void (^vv)(void) = ^{ existingBlock(); }
vv();
struct __block_literal_3 {
...; // existing block
};
struct __block_literal_4 {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal_4 *);
struct __block_literal_3 *const existingBlock; // 这里可以看到,在Block内部,是保持了外部的Block指针
};
void __block_invoke_4(struct __block_literal_2 *_block) {
__block->existingBlock->invoke(__block->existingBlock);
}
void __block_copy_4(struct __block_literal_4 *dst, struct __block_literal_4 *src) {
//_Block_copy_assign(&dst->existingBlock, src->existingBlock, 0);
_Block_object_assign(&dst->existingBlock, src->existingBlock, BLOCK_FIELD_IS_BLOCK);
}
void __block_dispose_4(struct __block_literal_4 *src) {
// was _Block_destroy
_Block_object_dispose(src->existingBlock, BLOCK_FIELD_IS_BLOCK);
}
static struct __block_descriptor_4 {
unsigned long int reserved;
unsigned long int Block_size;
void (*copy_helper)(struct __block_literal_4 *dst, struct __block_literal_4 *src);
void (*dispose_helper)(struct __block_literal_4 *);
} __block_descriptor_4 = {
0,
sizeof(struct __block_literal_4),
__block_copy_4,
__block_dispose_4,
};
这时候Block的数据结构是:
struct __block_literal_4 _block_literal = {
&_NSConcreteStackBlock,
(1<<25)|(1<<29),
__block_invoke_4,
& __block_descriptor_4 // 这里可以看到,在Block内部,是保持了外部的Block指针
existingBlock,
};
截取NSObject类型变量
在Clang中,NSObject类型变量被当做__attribute__((NSObject))
类型。Block截取NSObject对象时,同样会做一份const copy NSObject *
。
比如:
@interface MyObject : NSObject
- (void)sayMyObjectAddress
@end
@implementation MyObject
- (void)sayMyObjectAddress {
NSLog(@"Instance pointer address is %p, Instance address is %p", &self, self);
}
@end
MyObject *obj = [MyObject new];
[obj sayMyObjectAddress];
^{
[obj sayMyObjectAddress];
}();
输出为:
可以看到,当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_object_assign(&dst->objectPointer, src-> objectPointer, BLOCK_FIELD_IS_OBJECT);
}
void __block_dispose_foo(struct __block_literal_5 *src) {
_Block_object_dispose(src->objectPointer, BLOCK_FIELD_IS_OBJECT);
}
截取__block修饰的变量
鉴于我们上面所说的都是const copy,因此对于在Block中对于其截取变量的任何改变,都是不被允许的。如果我们要修改Block内部的值,编译器就会提示如下错误:
那如何在Block中修改截取变量的值呢?我们自然会想到对外部变量加上__block
修饰符。我们将上面代码改成下面的形式,则会顺利编译通过:
__block int b = 13;
NSLog(@"Outside Block, address of __block int b is %p, b = %d", &b, b);
blockType1 blk = ^{
b++;
NSLog(@"Inside Block, address of __block int b is %p, b = %d", &b, b);
};
blk();
NSLog(@"After Block, address of __block int b is %p, b = %d", &b, b);
输出为:
这里会发现一个有意思的现象,虽然在进入Block前后,b的地址并不一样!** 也就是在进入Block前后,其实会有两个不同的b **。
之所以会这样,与Clang对于__block类型变量的处理有关。
当变量被标记为__block类型时,Clang会对变量b进行改写成一个如下格式的struct:
struct _block_byref_foo {
void *isa; // 设置为NULL
struct Block_byref *forwarding; // Block外部变量的地址
int flags; //refcount;
int size; // size of _block_byref_foo
typeof(marked_variable) marked_variable; // copy of Block 外部变量
};
比如:
int __block i = 10;
i = 11;
会被Clang改写做:
struct _block_byref_i {
void *isa;
struct _block_byref_i *forwarding;
int flags; //refcount;
int size;
int captured_i;
} i = { NULL, &i, 0, sizeof(struct _block_byref_i), 10 };
i.forwarding->captured_i = 11;
可以看到,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), ,
__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
。
这里的输出为:
题目二. 下面的代码会 正常输出/编译错误/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";
-
当在Block中操作str=@"World"时,相应的__block结构体会拷贝到heap上,同时,stack上的__block结构体的forwarding指针也会指向heap上的那份copy:
因此,在Block外面再次输出str的内容时,由于这时候stack上__block结构体的forwarding指针已经指向了heap上的__block结构体,因此也会输出heap上的captured_str指针所指向的内容:
@“World”
。
为了验证我们的猜测,我们可以用如下代码:
在进入Block前,Block中,进入Block后分别设置断点,并打印aR
指针的地址&aR
,会得到如下结果:
可以看到,在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
。
上面这段代码会正常输出,其结果为:
至于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中,并没有更改指针指向的地址,而仅仅是改变了指针指向地址中的值,这个操作是允许的。
其输出结果为:
同样的,类似还有下面代码,也是可以正常运行,并输出名字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
至于原因,相信大家都会知道了:)