您的位置:首页 > 移动开发 > IOS开发

和block循环引用说再见

2016-07-22 10:29 471 查看
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类型的变量指向self
NSLog(@"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要特别当心循环引用的问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  iOS block 循环引用