Fork me on GitHub

block为什么要用copy来修饰

前言

前段时间因为打算换工作,本来以为我已经复习好做好面试的准备了,结果约了一场面试,面试官问我了这个问题,我竟然回答得不知所措,所以赶紧学习脑补一下

关于block为什么要使用copy修复符,首先我们要了解一下关于栈区和堆区的概念

  • 内存的栈区:由编译器自动分配和释放,存放函数的参数值,局部变量的值等,其操作类似数据结构的栈。
  • 内存的堆区:一般由程序员分配和释放,若程序员不释放,程序结束时可能有OS进行回收,注意这里的内存堆区与数据结构中的堆是两回事,分配方式倒是类似于链表。

在iOS中block的类型

想必很多开发人员知道一般用copy修饰block,但是为什么要使用copy来修饰呢,其实在Objective-C语言中,一共有3中类型的block:

  • _NSConcreteGlobalBlock 全局的静态 block,不会访问外部局部变量(显然包括无外部变量或者全局变量)
  • _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  • _NSConcreteMallocBlock 保存在堆中的 block,当应用计数为 0 时。

测试代码代码(摘自网络):

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic,copy)void(^demoBolck)();

@property (nonatomic,strong)void(^demoBolck1)();

@end

@implementation ViewController

int b=8;//全局变量

- (void)viewDidLoad {

[super viewDidLoad];

void (^demoBolck)() = ^{

NSLog(@"indemoBolck");

};

NSLog(@"demoBolck %@",demoBolck); //<__NSGlobalBlock__: 0x1085af0e0> 无论ARC还是MRC下,因不访问外部局部(包括无外部变量或者只有全局变量),NSGlobalBlock表示在全局区

void (^demoBolck4)() = ^{

NSLog(@"indemoBolck4 %d",b);

};

NSLog(@"demoBolck4 %@",demoBolck4); //<__NSGlobalBlock__: 0x10150b120> 全局区

__block int a = 6; //block内部引用a,并修改其值,需要用block修饰,不然可以不用

void (^demoBolck2)() = ^{

NSLog(@"indemoBolck2 %d",a++);

};

demoBolck2();

NSLog(@"demoBolck2 %@,%d",demoBolck2,a); //<__NSMallocBlock__: 0x600000056c50> ARC下堆区,在ARC模式下,系统也会默认对Block进行copy操作,Block的内存地址这时候便显示在堆区 <__NSStackBlock__: 0x7fff5d0ada28>MRC下在栈区

NSLog(@"demoBolck2.Copy %@",[demoBolck2 copy]); //<__NSMallocBlock__: 0x600000056c50>copy操作不管MRC或者ARC都在堆区,只是在MRC下进行copy会改变地址

self.demoBolck = demoBolck2;

NSLog(@"self.demoBolck %@",self.demoBolck);//堆区<__NSMallocBlock__: 0x608000052630>

self.demoBolck1 = demoBolck2;

self.demoBolck1(); //demoBolck2 7 能执行无问题

NSLog(@"self.demoBolck1 %@",self.demoBolck1); //<__NSMallocBlock__: 0x600000056c50> strong修饰ARC和MRC都并没有问题, 但是assign和retain在MRC环境下是还是在栈区的,会有问题

}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

//注意:MRC环境下:demoBolck1用assign,retain修饰,栈区:<__NSStackBlock__: 0x7fff50915a50>,提前释放了所以运行到下面语句程序会崩溃。ARC环境下:用copy,strong, assign, retain修饰是可以正常打印出结果的。 无论什么环境,用copy,strong修饰是可以正常打印出结果的

self.demoBolck1();

}

@end

注意:测试代码需要把创建的类对象改成MRC才能打印到对应的数据 -fno-objc-arc

关于为什么block是copy来修饰,这涉及到block作用域的问题

首先,block是一个对象,所以block理论上是可以retain/release的,但是block在创建的时候它的内存默认是分配在栈(stack)上,而不是堆(heap)上的。所以它的作用域仅限创建时候的当前上下文(函数,方法,…),当你在该作用域外调用该block时,程序就会崩溃,所以为什么block需要使用copy来修饰,其实目的就是为了把它从内存的栈上挪动到内存的堆上,把其作用域扩大。

  • 官方文档:

Alt text

总结

  • block内部没有调用外部局部变量时存放在全局区(ARC和MRC下均是)
  • block使用了外部局部变量,这种情况下也是我们平时所常用的方式。MRC:block的内存地址显示在栈区,栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序奔溃,在对block进行copy后,block存在堆区,所以在使用block属性时使用copy修饰,但是在ARC中的block都会在堆上,系统会默认对block进行copy操作
  • 用copy,strong修饰block在ARC和MRC都是可以的,都是在堆区

补充

一个block要使用self,会处理成在外部声明一个weak变量指向self,然而为何会出现在block里又声明一个strong变量指向weaSelf?
原因:block会把写在block里的变量copy一份,如果直接在block里使用self,(self对变量默认是强引用)self对block持有,导致循环引用,所以这里需要声明一个弱引用weakSelf,让block应用weakSelf,打破循环应用。
而这样会导致另外一个问题,因为weakSelf是对self的弱应用,如果这个时候控制器pop或者其他方式导致引用计数变为0,就会被释放,如果这个block是异步调用而且调用的时候self已经释放了,这个时候weakSelf已经变成了nil.
当控制器(也可以是其他对象)pop回来之后(或者一些其他的原因导致释放),网络请求完成,如果这个时候需要控制器做出反映,需要strongSelf再对weakSelf强引用一下。
但是,你可能会有疑问,strongSelf对weakSelf强引用,weakSelf对self弱引用,最终也不是对self进行了强引用,会导致循环引用,答案是不会的,因为strongSelf是在block里面声明的一个指针,当block执行完毕后,strongSelf会释放,这时候讲不再强引用weakSelf,所以self会正确释放。

注意

  • 一般情况下你不需要自行调用copy或者retain一个block,只有当你需要在block定义域以外的地方使用时才需要copy,copy将block从内存栈区移到堆区。
  • 其实block使用copy是MRC留下来的也算是一个传统吧,在MRC下,如上述,在方法中的block创建在栈区,使用copy就能把他放到堆区,这样在作用域外调用block程序就不会奔溃。
  • 在ARC下,使用copy与strong其实都一样,因为block的retain就是用copy来实现的,所以block使用copy还能装装逼,所以自己是从MRC下走过来的。