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
_weak_
变量值赋值给它。 然而block是异步执行的,还没等到他执行,helper示例过了执行域就被释放了(这点通过打印结果也能看出来),因此当执行 @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循环引用说再见相关推荐

  1. iOS开发笔记(二):block循环引用

    写这篇文章的缘由是第一次面试时被问到了block循环引用的问题,当时回答的不是很好,首先要明确的是,block是否用copy修饰决定不了循环引用的产生,在此再一次进行补强,有不对的地方还请多多指教. ...

  2. Block循环引用问题(Objective-c)

    造成循环引用的简单理解是:Block的拥有者在Block作用域内部又引用了自己,因此导致了Block的拥有者永远无法释放内存,就出现了循环引用的内存泄漏 示例代码 @interface ObjTest ...

  3. iOS block循环引用问题深究

    对象A持有对象B,调用B的block参数方法,在里面使用了self.在使用block我们都会默认在里面使用weakself,网上搜了很多解释都是为了防止循环引用,以防self被持有导致内存泄露. 那么 ...

  4. block为什么用copy以及如何解决循环引用

    在完成项目期间,不可避免的会使用到block,因为block有着比delegate和notification可读性更高,而且看起来代码也会很简洁.于是在目前的项目中大量的使用block. 之前给大家介 ...

  5. Block的循环引用

    2019独角兽企业重金招聘Python工程师标准>>> 在ios常见的循环引用中曾经提到过block: 看看上面最基本的block循环应用,self包含block,block包含了s ...

  6. 相亲app开发,解决内存循环引用的问题

    循环引用是什么 ARC已经出来很久了,自动释放内存的确很方便,但是在相亲app开发应用中,并非绝对安全绝对不会产生内存泄露.导致iOS对象无法按预期释放的一个无形杀手是--循环引用.循环引用可以简单理 ...

  7. block(六)循环引用-b

    在ARC与非ARC环境下对block使用不当都会引起循环引用问题,一般表现为,某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身,简单说就是self.theBloc ...

  8. 关于Block的copy和循环引用的问题

    http://blog.csdn.net/felix9/article/details/9619313 在实际开发中,发现使用Block有着比delegate和notification更简洁的优势.于 ...

  9. iOS进阶之底层原理-block本质、block的签名、__block、如何避免循环引用

    面试的时候,经常会问到block,学完本篇文章,搞通底层block的实现,那么都不是问题了. block的源码是在libclosure中. 我们带着问题来解析源码: blcok的本质是什么 block ...

最新文章

  1. Altium Designer飞线(未连接线)不显示的解决方法
  2. Redis进阶-如何发现和优雅的处理BigKey一二事
  3. givemesomecredit数据_你是如何走上数据分析之路的?
  4. Android动态改变TextView字体颜色
  5. oracle导入大量csv_Oracle导入数据到表(支持插入大量数据)
  6. (转)SpringMVC学习(二)——SpringMVC架构及组件
  7. html2canvas图片坐标,html2canvas生成的图片偏移不完整的解决方法
  8. 《中国人工智能学会通讯》——6.7 实体链接任务及系统
  9. Linux基础命令---ftp
  10. 21.5 英寸 iMac 供应减少:是新品发布的节奏吗?
  11. Flink流式计算在节省资源方面的简单分析
  12. 如不指定存储类型c语言,总结C语言的五种存储类型
  13. 智能监狱管理系统APP软件开发
  14. Python Turtle画奥运标志
  15. 高新技术背景下超大城市垃圾处理的成本控制研究
  16. 分享11个网页游戏和9个黑客源码,总有一款适合你
  17. day 05 random time sys os pickle json re模块 爬取dytt
  18. 高并发下如何设计秒杀系统?
  19. 电脑搜索文件的服务器,Archivarius注册版
  20. wince 触摸屏 学习

热门文章

  1. 2020年中国互联网公司薪酬排行榜发布了!这些你都知道吗?
  2. Linux WIFI模块驱动移植
  3. Django-路由管理 path view
  4. SQLException: #22001你知道这个错误码吗
  5. 什么是NP-Hard
  6. 前端开发者应该知道的 Centos/Dokcer/Nginx/Node/Jenkins 操作( 长文)
  7. 四个角不是直角的四边形_四边形的特点是有四条直的边和四个直角对吗
  8. 浅谈“决策引擎”在身份管理的应用
  9. HanLP《自然语言处理入门》笔记--1.新手上路
  10. 将linux的系统时间EDT改为CST