和block循环引用说再见

来源:互联网 发布:rust优化补丁 编辑:程序博客网 时间:2024/04/30 13:08

to be block? or to be delegate?

这是一个钻石恒久远的问题。个人在编码中暂时没有发现两者不能通用的地方,习惯上更偏向于block,没有什么很深刻的原因,只是认为block回调写起来更便捷,直接在上下文中写block回调使得代码结构更清晰,可读性更强。而delegate还需要申明protocol接口,设置代理对象,回调方法与上下文环境不能很好契合,维护起来没有block方便。另外初学者很容易会被忘记设置代理对象坑…

然而惯用block是有代价的,最大的风险就是循环引用,这个问题一旦没有处理好就会造成内存泄露,而且问题很难被发现。本篇将以简单的demo谈谈ARC下block循环引用产生的原因以及避免block的循环引用。


ARC场景分析

场景一
来看看最简单的block循环引用的案例。由一个控制器自己申明一个block属性,执行block打印自己的另一个成员变量。上代码:

//MyViewController.mtypedef 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要特别当心循环引用的问题。

3 0
原创粉丝点击