写这篇文章的缘由是第一次面试时被问到了block循环引用的问题,当时回答的不是很好,首先要明确的是,block是否用copy修饰决定不了循环引用的产生,在此再一次进行补强,有不对的地方还请多多指教。
1.block为什么要用copy修饰
1.1 内存堆栈理解
- 内存栈区
由编译器自动分配释放,存放函数的参数值,局部变量的值等,不需要程序员来操心。其操作方式类似于数据结构中的栈。
- 内存堆区
一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。尽管后边苹果引入了ARC机制,但是ARC的机制其实仅仅是系统帮助程序员添加了retain,release,autorelease代码,并不是说系统就可以自动管理了。他的系统管理的原理还是MRC,并没有本质区别。注意内存堆区与数据结构中的堆是两回事,分配方式倒是类似于链表。
1.2 block作用域
首先,block是一个对象,所以block理论上是可以retain/release的。但是block在创建的时候它的内存是默认是分配在栈(stack)上,而不是堆(heap)上的。所以它的作用域仅限创建时候的当前上下文(函数, 方法...),当你在该作用域外调用该block时,block占用的内存已经释放,无法进行访问,程序就会崩溃,出现野指针错误。
1.3 三种block
-
NSGlobalBlock:全局的静态block,没有访问外部变量,存储在代码区(存储方法或者函数)。他直到程序结束的时候,才会被被释放。但是我们实际操作中基本上不会使用到不访问外部变量的block。
void(^testOneBlock)() = ^(){ NSLog(@"我是全局的block"); }; NSLog(@"testOneBlock=%@",testOneBlock); //控制台输出 2017-06-10 09:45:09.767 ReactiveCocoa[871:14517] testOneBlock=<__NSGlobalBlock__: 0x1045982d0> //全局block,他会随程序销毁而销毁 复制代码
-
NSStackBlock:保存在栈中的block,没有用copy去修饰并且访问了外部变量。但是必须要在MRC的模式下控制台才会输出NSStackBlock类型。
//需要MRC模式 int a = 5; void(^testTwoBlock)() = ^(){ NSLog(@"%d",a); }; NSLog(@"testTwoBlock=%@",testTwoBlock); //控制台输出 2017-06-10 09:45:09.768 ReactiveCocoa[871:14517] testTwoBlock=<__NSStackBlock__: 0x7fff5b668770> //栈区block,函数调用完毕就会销毁 复制代码
-
NSMallocBlock:保存在堆中的block,此类型blcok是用copy修饰出来的block,它会随着对象的销毁而销毁,只要对象不销毁,我们就可以调用的到在堆中的block。
int a = 5; self.block1 = ^(NSString *str, UIColor *color){ NSLog(@"%d",a); }; NSLog(@"block1=%@",self.block1); //控制台输出 2017-06-10 10:02:35.107 ReactiveCocoa[1075:19674] block1=<__NSMallocBlock__: 0x60000004ee50> //用copy修饰的不会函数调用完就结束,随对象销毁才销毁,这种是在开发中正确使用block的姿势 复制代码
第三种block在有些情况下会造成block的循环引用,将在下面进行讨论。
1.4 另一种理解方式:函数返回
关于函数返回,在一个函数的内部,return的时候返回的都是一个拷贝,不管是变量、对象还是指针都是返回拷贝,但是这个拷贝是浅拷贝。在这里我需要理解以下两点:
- 对于直接返回一些基本类型的变量来说,直接返回值的拷贝就好,没有问题。
- 对于返回一些非动态分配(new/malloc)得到的指针就可能出现问题,因为尽管你返回了这个指针地址。但是这个指针可能指向的栈内存,栈内存在函数执行完毕后就自动销毁了。如果销毁之后你再去访问,就会访问坏内存会导致程序崩溃。
明确上边两点之后,我们再来说,在MRC下,如果一个block作为参数,没有经过copy就返回。后果是什么呢?由于return的时候返回的是浅拷贝,也就是说返回的是对象的地址,因为在返回后这个block对应的栈内存就销毁了。如果你多次调用这个block就会发现,程序会崩溃。崩溃原因就是上边所说,block占用的空间已经释放了,你不可以进行访问了。
解决方案:就是在返回的时候,把block进行拷贝作为参数进行返回。这样做的好处是返回的那个block存储空间是在堆内,堆内的空间需要程序员自己去释放,系统不会自动回收,也就不会出现访问已释放内存导致的崩溃了。也就是我们在MRC下需要使用copy修饰符的原因。(此处是否是通过深复制在堆中申请内存不求甚解,在此标记,继续深究)
1.5 ARC下block用什么修饰
首先前面讲的内容都是在MRC下,MRC下block需要用copy修饰,但是在ARC下使用copy或strong修饰其实都一样,因为block的retain就是用copy来实现的。
2.block循环引用
在开始之前我们需要明确一点:是不是所有的block,使用self都会出现循环引用?其实不然,系统和第三方框架的block绝大部分不会出现循环引用,只有少数block以及我们自定义的block会出现循环引用。而我们只要抓住本质原因就可以了,如下:
如果block没有直接或者间接被self存储,就不会产生循环引用。就不需要用weak self。(retainCount无法变为0)
2.1 直接强引用:self -> block -> self
由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。如下
typedef void(^block)();
@property (copy, nonatomic) block myBlock;
@property (copy, nonatomic) NSString *blockString;
- (void)testBlock {
self.myBlock = ^() {
//其实注释中的代码,同样会造成循环引用
NSString *localString = self.blockString;
//NSString *localString = _blockString;
//[self doSomething];
};
}
复制代码
注:以下调用注释掉的代码同样会造成循环引用,因为不管是通过self.blockString还是_blockString,或是函数调用[self doSomething],因为只要block中用到了对象的属性或者函数,block就会持有该对象而不是该对象中的某个属性或者函数。
2.2 间接强引用:self -> 某个类 -> block -> self
间接强引用中,self并没有直接拥有block属性。来看下面一个例子:
这是一个持有block的view: XXSubmitBottomView
typedef void(^BtnPressedBlock)(UIButton *btn);
@interface XXSubmitBottomView : UIView
@property(strong,nonatomic)UILabel *allPriceLab;
@property(strong,nonatomic)UIButton *submittBtn;
@property(nonatomic, weak)XXConfirmOrderController *currentVc;
@property(nonatomic, weak)XXConfimOrderModel *model;
@property(nonatomic, copy)BtnPressedBlock block;
-(void)submittBtnPressed:(BtnPressedBlock)block;
复制代码
这是一个持有bottomView属性的控制器: XXConfirmOrderController
@interface XXConfirmOrderController ()
@property(nonatomic, strong) XXConfimOrderTableView *tableView;
@property(nonatomic, strong) XXSubmitBottomView *bottomView;
@property(nonatomic, strong) XXConfimOrderModel *confimModel;
@end
@implementation XXConfirmOrderController
-(void)viewDidLoad{
[super viewDidLoad];
self.title = @"确认下单";
self.view.backgroundColor = DDCJ_Gray_Color;
//UI
[self.view addSubview:self.tableView];
[self.view addSubview:self.bottomView];
//Data
[self loadData];
}
复制代码
下面是self.bottomView的懒加载以及block的回调处理
-(XXSubmitBottomView *)bottomView{
if (!_bottomView) {
_bottomView = [[XXSubmitBottomView alloc] initWithFrame:CGRectMake(0, self.view.height - 50, Width, 50)];
_bottomView.currentVc = self;
#warning self.bottomView.block self间接持有了BtnPressedBlock 必须使用weak!
WEAKSELF //ps: weakSelf的宏定义#define WEAKSELF typeof(self) __weak weakSelf = self;
[_bottomView submittBtnPressed:^(UIButton *btn) {
NSLog(@"do提交订单");
MBProgressHUD *hud = [MBProgressHUD showMessage:@"加载中..." toView:weakSelf.view];
NSMutableDictionary *dynamic = [NSMutableDictionary dictionary];
[dynamic setValue:weakSelf.confimModel.orderRemark forKey:@"orderRemark"];
if (weakSelf.agreementId) {
[dynamic setValue:weakSelf.agreementId forKey:@"agreementId"];
}
if (weakSelf.isShoppingCartEnter) {
[dynamic setValue:@"0" forKey:@"orderOrigin"];
}else{
[dynamic setValue:@"1" forKey:@"orderOrigin"];
}
[[APIClientFactory sharedManager] requestConfimOrderWithDynamicParams:dynamic success:^(NSMutableArray *dataArray) {
[hud hideAnimated:YES];
[weakSelf handlePushControllerWithModelList:dataArray];
} failure:^(NSError *error) {
[hud hideAnimated:YES];
[MBProgressHUD showError:error.userInfo[@"message"]];
}];
}];
}
return _bottomView;
}
复制代码
此处的控制器self并没有直接持有block属性,但是却强引用了bottomView,bottomView强引用了block属性,这就造成了间接循环引用。block回调内必须使用[weak self]来打破这个循环,否则就会导致这个控制器self永远都不会被释放掉产生常驻内存。
2.3 实际开发中的循环引用
使用通知(NSNotifation),调用系统自带的Block,在Block中使用self会发生循环引用。
注:自定义的block出现循环引用时都会出现警告,所以出问题时容易解决。但在这里,在block中的确出现了循环引用,也的确没有出现警告,这才是我们真正需要注意的,也是为什么我们需要理解block循环引用的原因。
2.4 解决办法
-
一般性解决办法
__weak typeof(self) weakSelf = self; 复制代码
通过__weak的修饰,先把self弱引用(默认是强引用,实际上self是有个隐藏的__strong修饰的),然后在block回调里用weakSelf,这样就会打破保留环,从而避免了循环引用,如下:
self -> block -> weakSelf self -> 某个类 -> block ->weakSelf 复制代码
提醒:__block与__weak都可以用来解决循环引用,但是,__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。__weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)。__block对象可以在block中被重新赋值,__weak不可以。
-
@weakify
@weakify(self) self.myBlock = ^() { NSString *localString = self.blockString; }; 复制代码
2.5 weak的缺陷
-
缺陷
如果我想在Block中延时来运行某段代码,这里就会出现一个问题,看这段代码:
- (void)viewDidLoad { [super viewDidLoad]; MitPerson*person = [[MitPerson alloc]init]; __weak MitPerson * weakPerson = person; person.mitBlock = ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakPerson test]; }); }; person.mitBlock(); } 复制代码
直接运行这段代码会发现[weakPerson test];并没有执行,打印一下会发现,weakPerson已经是 Nil 了,这是由于当我们的viewDidLoad方法运行结束,由于是局部变量,无论是MitPerson和weakPerson都会被释放掉,那么这个时候在Block中就无法拿到正真的person内容了。
-
解决办法一
- (void)viewDidLoad { [super viewDidLoad]; MitPerson*person = [[MitPerson alloc]init]; __weak MitPerson * weakPerson = person; person.mitBlock = ^{ __strong MitPerson * strongPerson = weakPerson; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [strongPerson test]; }); }; person.mitBlock(); } 复制代码
这样当2秒过后,计时器依然能够拿到想要的person对象。
深入理解
-
首先了解一些概念:
堆里面的block(被copy过的block)有以下现象:
1.block内部如果通过外面声明的强引用来使用,那么block内部会自动产生一个强引用指向所使用的对象。
2.block内部如果通过外面声明的弱引用来使用,那么block内部会自动产生一个弱引用指向所使用的对象。
-
这段代码的目的:
-
首先,我们需要在Block块中调用,person对象的方法,既然是在Block块中我们就应该使用弱指针来引用外部变量,以此来避免循环引用。但是又会出现问题,什么问题呢?就是当我计时器要执行方法的时候,发现对象已经被释放了。
-
接下来就是为了避免person对象在计时器执行的时候被释放掉:那么为什么person对象会被释放掉呢?因为无论我们的person强指针还是weakPerson弱指针都是局部变量,当执行完ViewDidLoad的时候,指针会被销毁。对象只有被强指针引用的时候才不会被销毁,而我们如果直接引用外部的强指针对象又会产生循环引用,这个时候我们就用了一个巧妙的代码来完成这个需求。
-
首先在person.mitBlock引用外部weakPerson,并在内部创建一个强指针去指向person对象,因为在内部声明变量,Block是不会强引用这个对象的,这也就在避免的person.mitBlock循环引用风险的同时,又创建出了一个强指针指向对象。
-
之后再用GCD延时器Block来引用相对于它来说是外部的变量strongPerson,这时延时器Block会默认创建出来一个强引用来引用person对象,当person.mitBlock作用域结束之后strongPerson会跟着被销毁,内存中就仅剩下了延时器Block强引用着person对象,2秒之后触发test方法,GCD Block内部方法执行完毕之后,延时器和对象都被销毁,这样就完美实现了我们的需求。
黑色代表强引用,绿色代表弱引用
-
-
-
解决办法二
- (void)viewDidLoad { [super viewDidLoad]; MitPerson*person = [[MitPerson alloc]init]; @weakify(self) person.mitBlock = ^{ @strongify(self) dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self test]; }); }; person.mitBlock(); } 复制代码
可以看出,这样就完美解决了weak的缺陷,我们可以在block中随意使用self。