block 的实现

#1.block:

从本质上来说,block是带有自动变量的匿名函数,是闭包在 OC 中的实现。

1.1.语法

#block表达式:

^ 返回值类型 (参数列表) {表达式}

#示例1.1:

1
2
3
^int(int a, int b) {
return a * b;
};

#block类型变量:

返回值类型 (^变量名)(参数列表) = Block表达式

#示例1.2:

1
2
3
int (^increment)(int) = ^(int count) {
return count + 1;
};

#block类型变量作为函数的参数:

1
2
3
4
- (int)blockAsParam:(int (^)(int a,int b))ablock
{
return ablock(2,3);
}

#block类型变量作为返回值:

1
2
3
4
5
- (int(^)(int a,int b))blockAsReturnValue{
return ^int (int x,int y){
return 0;
};
}

#完整示例1.3:

1
2
3
4
5
@interface UOBlocks : NSObject
- (void)callBlocks;
- (int)blockAsParam:(int (^)(int a,int b))ablock;//作为参数
- (int(^)(int a,int b))blockAsReturnValue;//作为返回值
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#import "UOBlocks.h"

typedef int(^multiBlock)(int a,int b);//宏定义

@interface UOBlocks()
//作为属性
@property (nonatomic, copy) multiBlock mMultiBlock;
@property (nonatomic, copy) int (^clickBlock)(int a);
@end

@implementation UOBlocks

- (void)callBlocks
{
self.mMultiBlock = ^int(int a, int b) {
return a * b;
};
NSLog(@"++++%d",self.mMultiBlock(2,3));

self.clickBlock = ^int(int a) {
return a;
};
self.clickBlock(5);
}

- (int)blockAsParam:(int (^)(int a,int b))ablock
{
return ablock(2,3);
}

- (int(^)(int a,int b))blockAsReturnValue{
return ^int (int x,int y){
return 0;
};
}
@end

#调用示例:

1
2
3
4
5
UOBlocks *blocks = [UOBlocks new];
[blocks callBlocks];
[blocks blockAsParam:^int(int a, int b) {
return a * b;
}];

#2.类型

  • NSGlobalBlock

全局的静态 block。不使用外部变量,或者只使用静态变量或全局变量。

  • NSStackBlock

保存在栈中的 block。内部引用局部变量或属性,且不能赋值给强引用或copy修饰的变量;

当函数返回时 block 会销毁。

  • NSMallocBlock

保存在堆中的 block。引用了局部变量或属性,且赋值给强引用或copy修饰的变量。

block 被 copy 时其 isa 指针会被修改为此类型;当引用计数为 0 时 block 会销毁。

三种类型的 block 分别被存储在全局区栈区堆区,需要注意的是在 ARC 环境中, block 的类型会与上面的定义有所差异,且往下看~

2.1.MRC

Xcode->Build Phases->Compile Sources找到AppDelegate.m,设置-fno-objc-arc,禁用其ARC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
int aGlobalVar = 10;
@interface AppDelegate()
@property (nonatomic, copy) void (^aCopyPropertyBlock)();
@end

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//On Global
static int aStaticVar = 2;
const int aConstVar = 3;
void (^aGlobalBlock)() = ^(){
aGlobalVar = 1;
aStaticVar = 3;
NSLog(@"%d",aConstVar);
};
NSLog(@"++aGlobalBlock info:%@",aGlobalBlock);

//On Stack
int var = 0;
void (^aStackBlock)() = ^{
NSLog(@"++%d",var);
};
NSLog(@"++aStackBlock info:%@",aStackBlock);

//copy
void (^aCopyBlock)() = [aGlobalBlock copy];
NSLog(@"++aCopyBlock info1:%@",aCopyBlock);

aCopyBlock = [aStackBlock copy];
NSLog(@"++aCopyBlock info2:%@",aCopyBlock);

//property
self.aCopyPropertyBlock = ^{
};
NSLog(@"++aCopyPropertyBlock info1:%@",self.aCopyPropertyBlock);

self.aCopyPropertyBlock = aStackBlock;
NSLog(@"++aCopyPropertyBlock info2:%@",self.aCopyPropertyBlock);

return YES;
}

输出日志:

1
2
3
4
5
6
++aGlobalBlock info:<__NSGlobalBlock__: 0x10600a8f8>
++aStackBlock info:<__NSStackBlock__: 0x7ffee9c35d30>
++aCopyBlock info1:<__NSGlobalBlock__: 0x10600a8f8>
++aCopyBlock info2:<__NSMallocBlock__: 0x6000020a4ae0>
++aCopyPropertyBlock info1:<__NSGlobalBlock__: 0x10600a958>
++aCopyPropertyBlock info2:<__NSMallocBlock__: 0x6000020bc990>

从示例及日志可以看出:

  • 未访问任何变量、或者只访问了全局变量、静态变量、常量的是 NSGlobalBlock
  • 访问了局部变量的是 NSStackBlock
  • NSGlobalBlock 的拷贝还是 NSGlobalBlock
  • NSStackBlock 的拷贝变成了 NSMallocBlock
  • 对于 copy 修饰的 block 属性,将其指向 NSStackBlock 类型的 block2 时,block2 会被拷贝到堆区,变成 NSMallocBlock

MRC环境中 block 的类型与前面讲到的定义相吻合;

另外对于 block 拷贝后得到的结果,其类型是否变化依据源 block 而定,并不是所有的 block 都会被拷贝到堆上~

2.2.ARC

删除之前在AppDelegate.m文件后的-fno-objc-arc,启用ARC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@interface AppDelegate()
@property (nonatomic, copy) void (^aCopyPropertyBlock)();
@property (nonatomic, strong) void (^aStrongPropertyBlock)();
@end

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//On Global
void (^aGlobalBlock)() = ^(){
};
NSLog(@"++aGlobalBlock info:%@",aGlobalBlock);

int var = 0;
// On Stack,引用局部变量,但赋值给weak变量,在栈上,StackBlock
void (^__weak aStackBlock)(void) = ^{
NSLog(@"+++%d",var);
};
NSLog(@"++aStackBlock:%@",aStackBlock);

//On heap,引用局部变量或属性,赋值给强引用变量,在堆上,MallocBlock
void (^aMallocBlock)() = ^{
NSLog(@"++%d",var);
};
NSLog(@"++aMallocBlock info:%@",aMallocBlock);

//copy
void (^aCopyBlock)() = [aGlobalBlock copy];
NSLog(@"++aCopyBlock from aGlobalBlock info1:%@",aCopyBlock);

aCopyBlock = [aStackBlock copy];
NSLog(@"++aCopyBlock from aStackBlock info2:%@",aCopyBlock);

//copy property
self.aCopyPropertyBlock = ^{
};
NSLog(@"++aCopyPropertyBlock from aGlobalBlock info1:%@",self.aCopyPropertyBlock);

self.aCopyPropertyBlock = aStackBlock;
NSLog(@"++aCopyPropertyBlock from aStackBlock info2:%@",self.aCopyPropertyBlock);

//strong property
self.aStrongPropertyBlock = ^{
};
NSLog(@"++aStrongPropertyBlock from aGlobalBlock info1:%@",self.aStrongPropertyBlock);

self.aStrongPropertyBlock = aGlobalBlock;
NSLog(@"++aStrongPropertyBlock from aGlobalBlock info2:%@",self.aStrongPropertyBlock);

self.aStrongPropertyBlock = aStackBlock;
NSLog(@"++aStrongPropertyBlock from aStackBlock info3:%@",self.aStrongPropertyBlock);

return YES;
}

输出日志:

1
2
3
4
5
6
7
8
9
10
++aGlobalBlock info:<__NSGlobalBlock__: 0x1058440e8>
++aStackBlock:<__NSStackBlock__: 0x7ffeea3bf8c0>
++aMallocBlock info:<__NSMallocBlock__: 0x60000269e070>
++aCopyBlock from aGlobalBlock info1:<__NSGlobalBlock__: 0x1058440e8>
++aCopyBlock from aStackBlock info2:<__NSMallocBlock__: 0x6000026f6490>
++aCopyPropertyBlock from aGlobalBlock info1:<__NSGlobalBlock__: 0x105844128>
++aCopyPropertyBlock from aStackBlock info2:<__NSMallocBlock__: 0x60000269ddd0>
++aStrongPropertyBlock from aGlobalBlock info1:<__NSGlobalBlock__: 0x105844148>
++aStrongPropertyBlock from aGlobalBlock info2:<__NSGlobalBlock__: 0x1058440e8>
++aStrongPropertyBlock from aStackBlock info3:<__NSMallocBlock__: 0x600002699800>

从示例及日志来分析:

  • 未访问任何变量的是 NSGlobalBlock
  • 访问了局部变量但赋值给”非”强引用对象的是 NSStackBlock
  • 访问了局部变量但赋值给强引用对象的是 NSMallocBlock
  • NSGlobalBlock 的拷贝还是 NSGlobalBlock
  • NSStackBlock 赋值给copy或strong修饰的block属性时,得到的是 NSMallocBlock

ARC中 block 的类型与前面的定义不再完全相同:

=后面引用了局部变量的block,赋值给默认修饰符为strong的aMallocBlock时,得到的是一个NSMallocBlock!也就是说ARC中 原本在栈上的 block 现在会被自动拷贝到堆上

#3.修饰符

block也是对象,作为属性时其修饰符可以使用copystrong。不过,相信很多人和我一样,从开始接受的教育就是使用copy。这一章节就来探讨为何这么做,示例分为MRCARC两种环境。

3.1.MRC

#示例3.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface AppDelegate()
@property (nonatomic, retain) void (^aBlock)();
@end

@implementation AppDelegate

- (void)retBlock{
self.aBlock = ^{
NSLog(@"++++aBlock");
};
}

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self retBlock];
NSLog(@"++aBlock info1:%@",self.aBlock);
self.aBlock();
return YES;
}
@end

输出日志:

1
2
++aBlock info1:<__NSGlobalBlock__: 0x1032f78f8>
++++aBlock

示例为MRC环境,block 属性aBlock声明时使用的修饰符为retain。因为没有引用局部变量,所以此 block 为NSGlobalBlock类型。运行之后程序正常执行和输出日志,即我们能在定义域外正常访问此NSGlobalBlock类型的 block

#示例3.2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)retBlock{
int var = 10;
self.aBlock = ^{
NSLog(@"++++aBlock:%d",var);
};
}

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self retBlock];
NSLog(@"++aBlock info1:%@",self.aBlock);
self.aBlock();
return YES;
}
@end

示例对retBlock方法稍作修改,即aBlock引用了局部变量,其他代码不变。运行之后程序会在“NSLog(@”++aBlock info1:%@”,self.aBlock);”处崩溃,错误信息为“EXC_BAD_ACCESS (code=1, address=0x28)”,即访问了已经被释放的内存。所以,我们不能在定义域外访问NSStackBlock类型的 block,即使将其保存到集合中,在定义域外访问时也会出现野指针问题。

这是因为 block 属性aBlock访问了局部变量,属于NSStackBlock,被分配在栈上。同时其修饰符为retain,只是引用关系并不改变内存分布。栈上的变量在出了当前定义域之后,随时可能被回收。所以在定义域外访问此 block 时,aBlock在栈上的内存已经被回收,最终发生崩溃。实际上在声明aBlock属性时,编译器就已经给出了警示”Retain’ed block property does not copy the block - use copy attribute instead”。

#示例3.3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface AppDelegate()
@property (nonatomic, copy) void (^aBlock)();//修饰符改为copy
@end

@implementation AppDelegate

- (void)retBlock{
int var = 10;
self.aBlock = ^{
NSLog(@"++++aBlock:%d",var);
};
}

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self retBlock];
NSLog(@"++aBlock info1:%@",self.aBlock);
self.aBlock();
return YES;
}
@end

示例中属性的修饰符改为copy,其他代码不变。运行后输出日志:

1
2
++aBlock info1:<__NSMallocBlock__: 0x600002adeb80>
++++aBlock:10

使用copy修饰符时,aBlock类型为NSMallocBlock,即被拷贝到了堆上,所以这次访问aBlock时,没有再出现野指针问题。

3.2.ARC

再来探讨一下ARC环境中 block 属性的修饰符问题。

#示例3.4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@interface AppDelegate()
@property (nonatomic, strong) void (^aBlock1)();
@property (nonatomic, strong) void (^aBlock2)();
@property (nonatomic, copy) void (^aBlock3)();
@end

@implementation AppDelegate

- (void)retBlock{
int var = 10;
self.aBlock1 = ^{
NSLog(@"++++aBlock1");
};
self.aBlock2 = ^{
NSLog(@"++++aBlock2:%d",var);
};
self.aBlock3 = ^{
NSLog(@"++++aBlock3:%d",var);
};
}

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self retBlock];
NSLog(@"++info1:%@",self.aBlock1);
NSLog(@"++info2:%@",self.aBlock2);
NSLog(@"++info3:%@",self.aBlock3);
return YES;
}
@end

输出日志:

1
2
3
++info1:<__NSGlobalBlock__: 0x10faa28f0>
++info2:<__NSMallocBlock__: 0x6000029edbc0>
++info3:<__NSMallocBlock__: 0x6000029ed9e0>

程序正常运行,可以看到ARC环境下 block 属性不论是否访问了局部变量,在其定义域外都能正常访问和调用。且使用strong修饰符时 block 的类型也会自动变成NSMallocBlock,效果与使用copy时一样。

3.3.小结

综上,block属性使用copy修饰符最为稳妥:

1、没有引用局部变量的 block 是NSGlobalBlock类型,使用retain、strong和copy修饰符时,都能正常在其定义域外访问和调用;

2、引用了局部变量的 block,MRC 中属于NSStackBlock类型,被保存在栈上随时可能被回收,使用retain修饰符时,在其定义域外调用会发生野指针问题,除非使用 copy 将其拷贝到堆上

3、引用了局部变量的 block,ARC 中赋值给 strong 和 copy 属性时,属于NSMallocBlock类型,都能在其定义域外正常访问和调用。且 strong 修饰的 block 会自动将源 block 拷贝到了堆上,效果与 copy 一样。

#4.编译与实现

新建 command line tool 工程,在main.m文件中写一个 block 的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
//main.m
#include <stdio.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int count = 10;
void (^ blk)(void) = ^(){
printf("varable in block:%d",count);
};
blk();
}
return 0;
}

使用 LLVM 编译器的clang命令将此段 OC 代码转换成 C++ 的源代码,以查看 block 的具体实现方式:

1
clang  -rewrite-objc  main.m

编译后会得到一个main.cpp文件,文件内容比较长,详细内容可到这里 查看,这里只看重点的部分:

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

这个__block_impl就是 block 的结构体。block 在编译过程中会被当做结构体进行处理,这里的isa字段指向 block 所属的类型,前面有具体的介绍;FuncPtr字段表示函数指针,即对应的是 block 中的{ }部分,它会指向下面编译器给我们生成的静态函数__main_block_func_0

1
2
3
4
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int count = __cself->count; // bound by copy
printf("varable in block:%d",count);
}

这个静态函数__xxx_block_func_0是一个函数的实现(前缀会根据.m文件的命名而变),对应的是 block 中{ }的部分。

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int count;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _count, int flags=0) : count(_count) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

__main_block_impl_0是该 block 的具体实现。此结构体中impl字段是__block_impl结构体的实例,__main_block_impl_0是对__block_impl的进一步封装;count字段是我们引入的外部变量;Desc即结构体的描述信息。最后结构体内部还提供了一个初始化函数__main_block_impl_0(),函数内对相关字段进行了初始化。

1
2
3
4
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

这是一个__main_block_desc_0结构体,用来描述__main_block_impl_0结构体的信息。其中的Block_size字段用于记录结构体大小;后面的__main_block_desc_0_DATA就是新生成的__main_block_desc_0实例。

1
2
3
4
5
6
7
8
9
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int count = 10;
void (* blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}

这是main.m文件中 main 函数编译后的具体内容,看上去有点复杂,下面逐步解析这段内容具体做了什么:

1
__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count))

#1、通过__main_block_impl_0()构造器创建了一个__main_block_impl_0结构体的实例。参数__main_block_func_0正是上面提到的函数实现,对应 block的 { }里的内容;__main_block_desc_0_DATA__main_block_desc_0结构体的实例,描述__main_block_impl_0结构体的大小信息;参数count是我们之前通过int count = 10声明的局部变量,这里作为值类型传进来。

1
&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count))

#2、在上一步的基础上加了&运算符,用来取上面刚创建的__main_block_impl_0结构体实例的地址。

1
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count))

#3、把实例地址转为一个函数地址。

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

#4、这一整句的作用就是:定义一个函数指针,指向一个新创建的__main_block_impl_0结构体的实例的地址。

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

#5、这是 block 的调用blk();在编译后的结果,通过函数指针 blk 调用函数的实现FnucPtr

#5.变量的捕获

5.1.捕获的实现

上面#4章节的示例中,__main_block_impl_0 结构体中impl.isa指向的是NSConcreteStackBlock类型。这是因为 main.m 中我们在 block 外声明了一个变量int count = 10,并且在 block 内部访问了该变量,所以 block 实例被保存在了栈上。

当然,如果我们在 block 内不访问任何局部变量,impl.isa应该是指向NSConcreteGlobalBlock类型,但实际上 clang 编译出来的结果却是指向NSConcreteStackBlock类型:

1
2
3
4
5
6
7
8
9
//main.m
#include <stdio.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
^(void){
};
}
return 0;
}

编译结果:

1
2
3
4
5
6
7
8
9
10
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

这和 clang 改写的实现及 ARC 有关,关于这个问题可以参考 唐巧 的博客,这里有具体的解释。

比较访问和不访问局部变量的两个版本编译后的main.cpp文件,注意构造函数__main_block_impl_0()这部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//访问局部变量
__main_block_impl_0(void *fp,
struct __main_block_desc_0 *desc,
int _count, int flags=0) : count(_count) {
//略
}
};

//未访问局部变量
__main_block_impl_0(void *fp,
struct __main_block_desc_0 *desc,
int flags=0) {
//略
}
};

访问局部变量时,构造函数__main_block_impl_0()的参数里多出一个_count,这里就是 block 对变量的自动捕获。在声明 block 时,栈上局部变量count的值被复制给_count,继而以参数的形式传入构造函数中,再被复制到__main_block_impl_0结构体中。

在block中访问的外部变量是复制过去的,写操作不对原变量生效。

5.2.可修改性

捕获后可以修改的变量:

  • 静态变量
  • 全局变量
  • 静态全局变量

捕获后不可以修改的变量:

  • 局部变量

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int aGloableVal = 1;//全局变量
static int aStaticGloable = 2;//静态全局变量

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
static int aStaticVal = 3;//静态变量
int anInt = 0;//局部变量
NSMutableArray *array = [NSMutableArray array];//可变数组

void (^blockT)(int) = ^(int blockVal){
aGloableVal = 8;
aStaticGloable = 9;
aStaticVal = 10;//以上三个都可以正常赋值

//anInt = 1;
//array = [NSMutableArray array];
//以上两个赋值操作会报错:"variable is not assignable"!!
[array addObject:@"2"];//数组可以正常添加对象
NSLog(@"局部变量:%d,\n全局变量:%d,\n静态全局变量:%d,\n静态变量:%d,\n数组容量:%lu",
anInt,aGloableVal,aStaticGloable,aStaticVal,(unsigned long)array.count);
};
aGloableVal = 4;
aStaticGloable = 5;
aStaticVal = 6;
anInt = 7;
[array addObject:@"1"];

blockT(5);

return YES;
}
@end

输出日志:

1
2
3
4
5
局部变量:0,
全局变量:8,
静态全局变量:9,
静态变量:10,
数组容量:2

根据示例,可以得到以下几个信息:

1、全局变量静态变量静态全局变量在 block 内可以重新赋值,而局部变量不行。anInt 在 block 内部重新赋值时,编译器报错“variable is not assignable”。

2、全局变量静态变量静态全局变量在初始化时都有默认值,block 内重新赋值后,最终打印出的都是重新修改之后的值。而局部变量 anInt 初始化时 = 0,在调用 block 之前重新赋值 = 1;而 block 内打印的 anInt 的值仍为赋值前的0。这说明 block 内捕获的局部变量的值是 blok 声明之前的那个值。

3、局部变量array 赋值时报错“variable is not assignable”,而执行 addObject 操作却不会报错。所以,block 中使用到的局部变量可以执行操作而不能赋值。

4、block 捕获局部变量仅捕获 block 闭包里面会用到的值,其他用不到的变量,它并不会去捕获。

#6.问题来了

为啥全局变量、静态变量在 block 内能修改,而局部变量不能修改?

内存区域分为:堆区、栈区、全局区(数据区)、代码区。

  • 全局变量、静态全局变量都存储在全局区,所以在 block 内可以直接被修改。
  • 静态变量传递给 block 的是内存地址值,所以能在 block 里面直接改变值。
  • 局部变量是以的方式传递到 block 的构造函数里,block 内不能改变此自动变量的值。

想要在 block 中修改变量的值,有2种方式:

  • 传递内存地址指针到 block 中;
  • 改变存储区方式(如__block anInt);

下面看看通过__block修饰变量并在 block 内修改变量值的示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int count = 10;
void (^ blk)(void) = ^(){
count = 11;
printf("update varable in block:%d",count);
};
blk();
}
return 0;
}

通过 clang 编译后得到新的main.cpp文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding;
int __flags;
int __size;
int count;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_count_0 *count; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

可以看到源码中增加了一个__Block_byref_count_0类型的结构体,内部保存着我们用__block修饰的变量count。而__main_block_impl_0()构造函数中引用的是__Block_byref_count_0的结构体指针*_count。这里,结构体指针不能再修改了。但指针指向的内存空间里的值是可以修改的。因此,在 block 内再次对__block修饰的变量赋值时,实际上是通过修改变量对应的结构体指针内存空间里的 value 来修改变量的值。


相关参考:

#©Apple官方文档 及 #©翻译

#©唐巧

#©思否


block 的实现
https://davidlii.cn/2018/03/15/block.html
作者
Davidli
发布于
2018年3月15日
许可协议