Block(三)类型与copy

上篇文章讲述了Block对象如何捕获不同类型的变量,现在开始要追踪Block本身类型的变化,而且Block对象类型和捕获是有关联的。

一、类型

1.1 类型归纳

block类型

1.2 验证

下面是验证代码,代码在这里

注意!!!

我们此处验证是通过class方法获取其类型,也可以通过重写代码,但是重写代码得到的C++代码,并不能正确反映Block的类型,因为编译期和运行期还是有区别,重写并不能百分百对应运行期的逻辑。

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
int good = 1;
void (^globalBlock1)(void) = ^{
NSLog(@"%d", good);
NSLog(@"globalBlock1->ARC:__NSGlobalBlock__ MRC:__NSGlobalBlock__");
};

- (void)testBlockType
{
void (^globalBlock2)(void) = ^{
NSLog(@"globalBlock2->ARC:__NSGlobalBlock__ MRC:__NSGlobalBlock__");
};

// 注意:MRC下是__NSStackBlock,在ARC下会自动copy到堆,成为__NSMallocBlock
int age = 10;
void (^stackBlock1)(void) = ^{
NSLog(@"stackBlock1->ARC:__NSMallocBlock MRC:__NSStackBlock");
NSLog(@"%d", age);
};

// 注意:^{ NSLog(@"temp block-> ARC:__NSStackBlock MRC:__NSStackBlock"); NSLog(@"%d", age) } 为自动变量
// 在此处为测试ARC情况下的Stack Block
NSLog(@"class: %@ %@ %@ %@",[globalBlock1 class], [globalBlock2 class], [stackBlock1 class], [^{
NSLog(@"stackBlock2-> ARC:__NSStackBlock MRC:__NSStackBlock");
NSLog(@"%d", age);
} class]);
}

根据以上代码,我们得到以下验证结果:

  • globalBlock1/globalBlock2
    • MRC:_NSGlobalBlock\_
    • ARC:_NSGlobalBlock\_
  • stackBlock1
    • MRC:__NSStackBlock__
    • ARC:__NSMallocBlock__
  • stackBlock2
    • MRC:__NSStackBlock__
    • ARC:__NSStackBlock__

我们看到,针对__NSGlobalBlock__MRCARC下的情形是一致的。

但是__NSStackBlock代码中赋值给stackBlock1在ARC下会自动转换为__NSMallocBlock

我们先梳理一下,什么情况下会将Block对象分配在栈上(__NSGlobalBlock),又是什么时候分配在数据段(__NSStackBlock)。

  • 访问了auto变量就分配在__NSStackBlock
  • 未访问auto变量将分配在__NSGlobalBlock__
  • __NSStackBlock进行copy,则会拷贝到__NSMallocBlock

为什么会这样?

这跟变量的内存位置有关,我们先复习上篇Block(二)本质与变量捕获中对变量捕获的情形:

block变量类型

​ 从上面的捕获情况,我们看看变量与Block类型存在什么关系?

​ 如果一个变量,可以通过直接访问(全局变量),或者指针访问(static局部变量),那么,通常只有一份拷贝,所有访问,都会访问该变量不变的内存地址,事实上也是这样。

​ 全局变量,存储于全局存储区,static全局变量也存在该区域(参考内存管理(一)引入)。这部分区域,由于该区域不需要在运行时才进行分配的地址,所以,在编译期就能确定,即加入Block捕获的是以上这种类型的变量,Block类型,同样为global性质,即__NSGlobalBlock__类型。

​ 现在理解捕获auto变量,Block类型对应为__NSStackBlock__NSMallocBlock,是不是简单多了。auto变量无法在编译期准确分配内存,在运行时捕获,当然会分配在对应栈或堆上。

二、copy

在开发中,我们捕获局部变量是很常见的事情,如果这种捕获导致,Block对象被分配到栈区,一旦脱离了变量的作用域,也就不能使用Block了,Block的使用就没那么便捷了。

如果分配在栈上的Block对象,进行copy操作,将会将Block对象,从栈上拷贝到堆上。

2.1 自动复制

在ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上,比如以下情况

  1. Block作为函数返回值时;
  2. 将Block赋值给__strong指针时;
  3. Block作为Cocoa API中,且方法名含有usingBlock的方法参数时;
  4. Block作为GCD API的方法参数时;

下面我们就针对以上1、2情况做如下验证,3、4不作验证:

2.1.1 函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef void (^MRCBlock)(void);

MRCBlock mrcblock()
{
int age = 10;
return ^{
NSLog(@"%d", age);
};
}

- (void)testStackBlockInMRC
{
MRCBlock block = mrcblock();
block();
NSLog(@"%@", [block class]);
}

以上代码在MRC下,会报错:

image-20181209051423894

可以看到,这种情况下Block在栈空间,会报错。

以上,在ARC情况下,再次运行,返回的Block类型,就是__NSMallocBlock__

2.1.2 赋值给__strong指针

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)testBlockWithStrong
{
int age = 28;
NSLog(@"%@", [^{
NSLog(@"stack block %d", age);
} class]);

void (^block)(void) = ^{
NSLog(@"malloc block %d", age);
};

NSLog(@"%@", [block class]);
}

以上代码,在ARC环境下,会输出:

Block类型与copy[82825:4315549] __NSStackBlock__

Block类型与copy[82825:4315549] __NSMallocBlock__

2.2 手动复制

通过调用copy方法,手动进行对Block完成从栈到堆的赋值。

2.2.1 属性写法

MRC下block属性的建议写法

1
@property (nonatomic, copy) void (^block)(void);

ARC下block属性的建议写法

1
2
@property (nonatomic, strong) void (^block)(void);
@property (nonatomic, copy) void (^block)(void);

三、 总结

block copy规律

参考

示例代码

  1. Block类型与copy