和block循环引用说再见
to be block? or to be delegate?
这是一个钻石恒久远的问题。个人在编码中暂时没有发现两者不能通用的地方,习惯上更偏向于block,没有什么很深刻的原因,只是认为block回调写起来更便捷,直接在上下文中写block回调使得代码结构更清晰,可读性更强。而delegate还需要申明protocol接口,设置代理对象,回调方法与上下文环境不能很好契合,维护起来没有block方便。另外初学者很容易会被忘记设置代理对象坑…
然而惯用block是有代价的,最大的风险就是循环引用,这个问题一旦没有处理好就会造成内存泄露,而且问题很难被发现。本篇将以简单的demo谈谈ARC下block循环引用产生的原因以及避免block的循环引用。
ARC场景分析
场景一
来看看最简单的block循环引用的案例。由一个控制器自己申明一个block属性,执行block打印自己的另一个成员变量。上代码:
//MyViewController.m
typedef void(^TestFn)(); //申明block变量类型
@interface MyViewController ()
@property (strong, nonatomic) NSObject *obj;
@property (copy, nonatomic) TestFn testFn; //申明block属性
@end@implementation MyViewController
- (void)viewDidLoad {[super viewDidLoad];self.obj = [[NSObject alloc]init];self.testFn = ^(){NSLog(@"%@",self.obj);};self.testFn();
}- (void)viewWillAppear:(BOOL)animated{[super viewWillAppear:animated];NSLog(@"push to stack");
}- (void)viewDidDisappear:(BOOL)animated{[super viewDidDisappear:animated];NSLog(@"pop from stack");
}- (void)dealloc{NSLog(@"myViewController dealloc");
}@end
事实上,上面代码会给出一个警告 Capturing 'self' strongly in this block is likely to lead to a retain cycle
告诉我们这样写将引发循环引用。
不如亲眼见证一下是否真的会循环引用。将该控制器push到一个导航控制器,然后pop出栈,查看dealloc方法是否被调用,若调用,则说明MyViewController被释放,并没有引起循环引用;若没被调用,则说明MyViewController无法释放,内存泄露。
push后控制台打印
2016-07-17 12:32:40.037 test[83585:2901002] <NSObject: 0x7fa76859ec80>
2016-07-17 12:32:40.038 test[83585:2901002] push to stack
pop后:
2016-07-17 12:32:40.037 test[83585:2901002] <NSObject: 0x7fa76859ec80>
2016-07-17 12:32:40.038 test[83585:2901002] push to stack
2016-07-17 12:33:42.375 test[83585:2901002] pop from stack
dealloc方法并没有被调用,课件控制器没有被释放,而内存泄露正是block造成的。原因在于,block会retain其内部的对象,在上面的代码中会retain self所指向的对象。同时block作为self的成员变量,会被self持有。这就造成了self和block彼此持有,谁都无法释放谁的局面,从而内存泄露。
暂且不说如何避免引用循环。这个例子中,XCode给出了循环引用的警告,方便我们发现捕捉问题。然而实际编码中,很多场景是没有警告的,不谨慎使用很难发现循环引用的存在。
场景二
经常会有点击一个cell上的按钮触发block回调的需求,为简单起见,这里以点击UIView上绑定的按钮触发block回调为例演示。先上代码:
自定义一个UIView对象MyView,暴露block回调属性
//MyView.h
#import <UIKit/UIKit.h>
typedef void(^XFTestFn)();
@interface MyView : UIView
@property (nonatomic, copy) XFTestFn testFn;
@end//MyView.m
#import "MyView.h"
@implementation MyView
- (instancetype)initWithFrame:(CGRect)frame
{self = [super initWithFrame:frame];if (self) {NSLog(@"myview instance created");UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];button.frame = CGRectMake(0, 0, 100, 40);button.backgroundColor = [UIColor orangeColor];[button setTitle:@"点击回调" forState:UIControlStateNormal];[button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];[button addTarget:self action:@selector(actionBack:) forControlEvents:UIControlEventTouchUpInside];[self addSubview:button];}return self;
}- (void)actionBack:(UIButton *)sender{if (self.testFn) {self.testFn();}
}- (void)dealloc{NSLog(@"myView dealloc");
}
@end
创建一个控制器MyViewController作为回调上下文
//MyViewController.m
@interface MyViewController ()
@property (strong, nonatomic) NSObject *obj;
@end@implementation MyViewController
- (void)viewDidLoad {[super viewDidLoad];self.obj = [[NSObject alloc]init];MyView *myView = [[MyView alloc]initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 300)];myView.testFn = ^(){NSLog(@"button callback: %@",self.obj);};[self.view addSubview:myView];
}- (void)viewWillAppear:(BOOL)animated{[super viewWillAppear:animated];NSLog(@"push to stack");
}- (void)viewDidDisappear:(BOOL)animated{[super viewDidDisappear:animated];NSLog(@"pop from stack");
}- (void)dealloc{NSLog(@"myViewController dealloc");
}
@end
运行后,push到MyViewController控制器,点击按钮回调,然后pop。控制台打印信息如下:
2016-07-17 17:51:40.298 test[84299:3026143] myview instance created
2016-07-17 17:51:40.299 test[84299:3026143] push to stack
2016-07-17 17:51:41.157 test[84299:3026143] button callback: <NSObject: 0x7fd1ca54b3c0>
2016-07-17 17:51:42.269 test[84299:3026143] pop from stack
发现myView的dealloc和myViewController的dealloc方法都被调用,可见上面代码同样产生了循环引用,尽管XCode没有给出警告。下图可以表明产生循环引用的原因:
场景三
将场景二中myViewController.m代码替换如下:
//MyViewController.m
@implementation MyViewController
- (void)viewDidLoad {[super viewDidLoad];MyView *myView = [[MyView alloc]initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 300)];myView.testFn = ^(){myView.backgroundColor = [UIColor purpleColor];};[self.view addSubview:myView];
}- (void)viewWillAppear:(BOOL)animated{[super viewWillAppear:animated];NSLog(@"push to stack");
}- (void)viewDidDisappear:(BOOL)animated{[super viewDidDisappear:animated];NSLog(@"pop from stack");
}- (void)dealloc{NSLog(@"myViewController dealloc");
}
@end
运行一趟打印如下:
2016-07-17 18:05:24.529 test[84327:3032859] myview instance created
2016-07-17 18:05:24.530 test[84327:3032859] push to stack
2016-07-17 18:05:28.563 test[84327:3032859] pop from stack
2016-07-17 18:05:28.563 test[84327:3032859] myViewController dealloc
可以看到,myViewController被释放了,然而myView的dealloc方法并没有调用。原因同样是因为产生了循环引用,元凶则是myView自己。myView持有自己的成员变量block,block在执行时对myView做了操作,因此retain了一下myView。这样,myView与block互相强引用,彼此无法释放。
打破循环引用
分析一下上面3个产生循环引用的场景,原因可以概括为:block中使用了持有或间接持有block的变量,所谓的持有就是强引用。因此要想打破循环引用,只要打破其中任意一个强引用即可。外部变量必然会copy一份block,那么只能对block中用到的变量做手脚,以使block不持有这个变量所指的对象。以场景二为例,在block外面添加一行代码:
- (void)viewDidLoad {[super viewDidLoad];self.obj = [[NSObject alloc]init];MyView *myView = [[MyView alloc]initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 300)];__weak typeof(self) weakSelf = self; //创建一个self对象的弱引用变量myView.testFn = ^(){NSLog(@"button callback: %@",weakSelf.obj);};[self.view addSubview:myView];
}
ok,运行一下打印如下:
2016-07-17 18:47:46.208 test[84381:3053425] myview instance created
2016-07-17 18:47:46.209 test[84381:3053425] push to stack
2016-07-17 18:47:48.122 test[84381:3053425] button callback: <NSObject: 0x7ffa6bdd7170>
2016-07-17 18:47:50.030 test[84381:3053425] pop from stack
2016-07-17 18:47:50.030 test[84381:3053425] myViewController dealloc
2016-07-17 18:47:50.031 test[84381:3053425] myView dealloc
surprised!视图和控制器在pop之后都被释放了,说明并没有产生循环引用。原因在于我们创建了一个self对象的弱引用变量,供block内部使用,因此block并不会强引用self对象。对象间的引用关系如下:
循环引用成功打破,我们的目的似乎已经达到了。然而,细心的童鞋会发现,block没有强引用对象,这样可能会产生一个问题:当block回调被执行的时候,其弱引用的对象随时都有可能被外部释放!为避免block在执行过程中相关的对象被释放,修改代码如下:
- (void)viewDidLoad {[super viewDidLoad];self.obj = [[NSObject alloc]init];MyView *myView = [[MyView alloc]initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 300)];__weak typeof(self) weakSelf = self;myView.testFn = ^(){__strong typeof(self) strongSelf = weakSelf; //在block内部创建一个strong类型的变量指向selfNSLog(@"button callback: %@",strongSelf.obj);};[self.view addSubview:myView];
}
上述代码在block开始执行的时候创建了一个变量strongSelf,强引用self对象。绕来少绕,似乎又绕回来了!其实不然,之前block强引用self对象是因为block在执行时copy了self对象的指针,只有当block本身释放时其对self的强引用才会撤销。而此处是在block内部创建了一个指向self的局部变量,是保存在栈上的,一旦block执行作用域结束,该变量就被自动释放了。因此并不会产生循环引用。对象间的关系如下:
@weakify, @strongify
@weakify和@strongify是一对非常好用的用于管理block循环引用的宏,定义于libextobjc框架的EXTScope文件中。对于上面的代码,只需要这样写:
- (void)viewDidLoad {[super viewDidLoad];self.obj = [[NSObject alloc]init];MyView *myView = [[MyView alloc]initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 300)];@weakify(self) //创建一个 self_weak 变量弱引用self对象myView.testFn = ^(){@strongify(self) //创建一个 局部self 变量强引用self对象NSLog(@"button callback: %@",self.obj);};[self.view addSubview:myView];
}
@weakify(self)
创建了一个 self_weak_ 变量弱引用self对象。
@strongify(self)
在block内部创建了一个局部变量 self 强引用 self_weak_ 指向的对象,即self对象。
因此这两个宏定义完全等价于:
__weak typeof(self) weakSelf = self;
__strong typeof(self) strongSelf = weakSelf;
不要碰到block就套@weakify, @strongify
@weakify的作用是为了避免block强引用self对象,@strongify的作用的保证block在执行的时候self对象不被外界所释放。然而,并不能保证block在执行之前self对象不被释放。创建下面一个继承自NSObject的类:
//AsynHelper.h
@interface AsynHelper : NSObject
- (void)doAsyncWork;
@end//AsynHelper.m
@interface AsynHelper ()
@property (nonatomic, strong) NSObject *obj;
@end@implementation AsynHelper
{NSObject *_obj;
}- (instancetype)init
{self = [super init];if (self) {NSLog(@"NetworkHelper instance created!");}return self;
}- (void)doAsyncWork{_obj = [[NSObject alloc]init];@weakify(self)dispatch_async(dispatch_get_main_queue(), ^{@strongify(self)NSLog(@"asyn called:%@",self.obj);});
}- (void)dealloc{NSLog(@"NetworkHelper instance dealloc");
}
@end
在控制器中通过init方法实例化一个对象,并调用doAsyncWork方法。
AsynHelper *helper = [[AsynHelper alloc]init];
[helper doAsyncWork];
执行后打印如下:
2016-07-21 18:26:02.749 test[37126:4701929] NetworkHelper instance created!
2016-07-21 18:26:02.750 test[37126:4701929] NetworkHelper instance dealloc
2016-07-21 18:26:02.764 test[37126:4701929] asyn called:(null)
可以看到,helper实例是被释放了,但是block执行的打印结果却为null。断点一调,发现当block执行的时候,self变量竟然为nil。揪其原因,全是异步惹的祸。@strongify(self)
是在block执行域创建了有个局部self变量,并把通过 @weakify(self)
创建的 self
变量值赋值给它。 然而block是异步执行的,还没等到他执行,helper示例过了执行域就被释放了(这点通过打印结果也能看出来),因此当执行
_weak_@strongify(self)
时,self_weak_
已为nil,自然创建的self变量也是nil。
最常见的异步执行block的情况应当是网络请求通过block异步回调了。因此在成对使用 @weakify
, @strongify
是应当确保当前对象不会轻易被释放,尤其是在临时创建的cocoa对象(集成字NSObject)中使用异步回调block,不出意外都会出现这个问题。
事实上,上面这段代码根本没有必要套 @weakify(self)
,@strongify(self)
,因为这个执行的block是临时的,当前对象并没有持有block,所以直接在block中使用self不会造成循环引用。那么问题又来了,哪些情况下使用block应当小心循环应用?
哪些场景下的block要当心循环运用
将block简单分类,有下面3种使用场景:
临时创建的。包括临时并执行的自定义申明的block类型变量,以及系统的例如数组enumerate遍历用到的block,这些block变量都是临时创建使用的,保存在栈上,出域便会自动释放,不存在引用循环的问题。
需要存储在堆上但只调用一次的。例如GCD的异步执行block、UIView动画执行完毕后的回调block等,这些block会在堆上保存。这类block的正确实现应当是block一旦执行完毕就置其为nil,这样就不存在循环引用的问题。
需要长期存储的。例如button点击回调block,这类block需要多次执行,需要长期存储。使用这种block要特别当心循环引用的问题。
和block循环引用说再见相关推荐
- iOS开发笔记(二):block循环引用
写这篇文章的缘由是第一次面试时被问到了block循环引用的问题,当时回答的不是很好,首先要明确的是,block是否用copy修饰决定不了循环引用的产生,在此再一次进行补强,有不对的地方还请多多指教. ...
- Block循环引用问题(Objective-c)
造成循环引用的简单理解是:Block的拥有者在Block作用域内部又引用了自己,因此导致了Block的拥有者永远无法释放内存,就出现了循环引用的内存泄漏 示例代码 @interface ObjTest ...
- iOS block循环引用问题深究
对象A持有对象B,调用B的block参数方法,在里面使用了self.在使用block我们都会默认在里面使用weakself,网上搜了很多解释都是为了防止循环引用,以防self被持有导致内存泄露. 那么 ...
- block为什么用copy以及如何解决循环引用
在完成项目期间,不可避免的会使用到block,因为block有着比delegate和notification可读性更高,而且看起来代码也会很简洁.于是在目前的项目中大量的使用block. 之前给大家介 ...
- Block的循环引用
2019独角兽企业重金招聘Python工程师标准>>> 在ios常见的循环引用中曾经提到过block: 看看上面最基本的block循环应用,self包含block,block包含了s ...
- 相亲app开发,解决内存循环引用的问题
循环引用是什么 ARC已经出来很久了,自动释放内存的确很方便,但是在相亲app开发应用中,并非绝对安全绝对不会产生内存泄露.导致iOS对象无法按预期释放的一个无形杀手是--循环引用.循环引用可以简单理 ...
- block(六)循环引用-b
在ARC与非ARC环境下对block使用不当都会引起循环引用问题,一般表现为,某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身,简单说就是self.theBloc ...
- 关于Block的copy和循环引用的问题
http://blog.csdn.net/felix9/article/details/9619313 在实际开发中,发现使用Block有着比delegate和notification更简洁的优势.于 ...
- iOS进阶之底层原理-block本质、block的签名、__block、如何避免循环引用
面试的时候,经常会问到block,学完本篇文章,搞通底层block的实现,那么都不是问题了. block的源码是在libclosure中. 我们带着问题来解析源码: blcok的本质是什么 block ...
最新文章
- Altium Designer飞线(未连接线)不显示的解决方法
- Redis进阶-如何发现和优雅的处理BigKey一二事
- givemesomecredit数据_你是如何走上数据分析之路的?
- Android动态改变TextView字体颜色
- oracle导入大量csv_Oracle导入数据到表(支持插入大量数据)
- (转)SpringMVC学习(二)——SpringMVC架构及组件
- html2canvas图片坐标,html2canvas生成的图片偏移不完整的解决方法
- 《中国人工智能学会通讯》——6.7 实体链接任务及系统
- Linux基础命令---ftp
- 21.5 英寸 iMac 供应减少:是新品发布的节奏吗?
- Flink流式计算在节省资源方面的简单分析
- 如不指定存储类型c语言,总结C语言的五种存储类型
- 智能监狱管理系统APP软件开发
- Python Turtle画奥运标志
- 高新技术背景下超大城市垃圾处理的成本控制研究
- 分享11个网页游戏和9个黑客源码,总有一款适合你
- day 05 random time sys os pickle json re模块 爬取dytt
- 高并发下如何设计秒杀系统?
- 电脑搜索文件的服务器,Archivarius注册版
- wince 触摸屏 学习
热门文章
- 2020年中国互联网公司薪酬排行榜发布了!这些你都知道吗?
- Linux WIFI模块驱动移植
- Django-路由管理 path view
- SQLException: #22001你知道这个错误码吗
- 什么是NP-Hard
- 前端开发者应该知道的 Centos/Dokcer/Nginx/Node/Jenkins 操作( 长文)
- 四个角不是直角的四边形_四边形的特点是有四条直的边和四个直角对吗
- 浅谈“决策引擎”在身份管理的应用
- HanLP《自然语言处理入门》笔记--1.新手上路
- 将linux的系统时间EDT改为CST