iOS block之循环引用

来源:互联网 发布:外国人对中国有知乎 编辑:程序博客网 时间:2024/05/21 17:56

此篇博文用到了上一篇的一些知识点,有需要了解可以点此链接《iOS block之三种block》


前言

首先还是从一个大家耳熟能详的循环引用的条件说起:有3个对象A、B、C,当A强引用B,B强引用C,C又一不小心强引用了A,就出现了循环引用。
举个常见的栗子如下:
这里写图片描述

上面的栗子中,A代表一个vc,B代表一个view,它是vc的property,C是个block,它是view的property。
A强引用了B,B强引用了C,如果C又强引用了A,即block中直接或间接引用了vc的强指针,则循环引用构成。

上面所说的直接引用,就是指block中直接使用了self。
间接引用,是指block通过强引用vc的property间接持有了vc。

要想解除ABC间的循环引用,最直接方法是拆掉一环。
继续使用上面的栗子:
这里写图片描述

即block中使用vc的弱指针,这样就可以打破循环引用。当vc释放的时候大家都可以顺利释放了。


主体

说到block与循环引用,我们将首先分析一下block的出现形式,然后针对每一种形式具体分析会不会产生循环应用,以及产生或不产生的原因。


block出现的形式主要有三种情况,
1、作为property出现

@property (nonatomic, strong) void(^block1)();@property (nonatomic, copy) void(^block2)();

2、作为函数的参数出现

- (void)saveFile:(NSDictionary *)dic complete:(void(^)(BOOL success))complete;

3、在函数内部定义,作为函数的一部分出现

- (void)xxx{    __block int i = 0;    void (^addBlock)(int) = ^(int a){        i = i + a;        NSLog(@"i = %d",i);    };    addBlock(5);}

结合平时的开发,上面的东东至少已经混了个脸熟了,下面就不同情况具体分析吧。


1、作为property出现。

@property (nonatomic, strong) void(^block1)();@property (nonatomic, copy) void(^block2)();

用copy关键字修饰block是有历史原因的,其实在ARC下使用copy和strong都可以,没啥区别,只是在MRC下,必须使用copy关键字才能有效持有block。
如果在MRC下使用strong修饰block变量,则必须使用Block_copy()方法才能有效持有block,而不是retain,因为block没有retain和release方法。
Block_copy()方法可以把block复制到堆区,以保证在定义block的函数执行完毕出栈后block依然存在,而不会因为野指针而导致程序崩溃。block没有引用计数的概念,因此使用strong变量持有block时并不会获得blcok的拷贝,当程序执行完毕出栈时,block就随着函数释放掉了,因此需要将block拷贝到内存的永久存储区——堆区,才能在block的宿主释放之前永久持有它。

在ARC环境下大致可以这样说,只要作为property的block出现,就可以断定它位于内存的堆区,是堆block,它的生存周期和它的宿主target是一样的。因为target和block是强引用关系,所以target释放了,作为property的block也跟着释放了,如果block强引用了别人释放不掉,则target也无法释放。

因此,此类block(堆区block)中若引用了self强持有的变量(包括self自己),都需要弱化处理,以免出现循环引用。

2、作为函数的参数出现

(我们这里只讨论block没有被其他对象持有的情况,稍后会讨论block参数被持有的情况)

- (void)saveFile:(NSDictionary *)dic complete:(void(^)(BOOL success))complete;

这类block一般作为函数的最后一个参数,当然也可以有两个或者更多block参数,如SDWebImage和UIView的一些方法就有两个。这类block仅仅是作为函数的参数,和函数的其他参数一样没有被某个对象持有,生命周期和该函数相同,函数入栈时创建,函数出栈时释放。
因为block的生命周期和函数相同,而函数一定会释放,因此这类block中可以使用不被弱化的外部对象。
当然也可以使用弱化的,它们之间还有一定的区别,我们稍后会做介绍。

3、在函数内部定义,作为函数的一部分出现

- (void)xxx{    __block int i = 0;    void (^addBlock)(int) = ^(int a){        i = i + a;        NSLog(@"i = %d",i);    };    addBlock(5);}

这种block在函数中定义,在函数中调用,类似于函数中的一个局部变量,生命周期和局部变量相同,函数入栈时创建,函数出栈后释放,这类block在实际开发过程中极少用到,也不会导致循环引用。


总结:

在正常的开发过程所遇到的以上三种形式的block中,只有作为property的block才可能导致循环引用。


后续

是不是有个问题还没说完?

就是作为函数参数的block,它也经常会被持有,想想都有哪些方法的block被持有了一会儿才调用的?



好了,不用思考了,我来说吧。
比如UIView的属性动画
比如AFN的网络请求
再比如SDWebImage的图片下载

以上三个对于每个iOS开发者应该都不会陌生吧~

下面就以AFN为例大致说说其实现请求以及完成后回调的原理吧

  • 首先,调用AFN的方法传入对应的参数(包括block的实现);
  • 之后,AFN内部会创建一个对象持有这个block,然后把这个对象添加进任务列表(一个“单例”所持有的字典,url作为key);
  • 请求完成后,根据这个url在任务列表中找出这个对象,调用这个对象所持有的block;
  • 最后,将这个对象移除,block释放。

在这样的操作中,如果block中使用self,它就会强持有self,导致self在block返回之前不能释放,但这种不能释放只是暂时的,因为不管成功还是失败或者超时,请求迟早会结束,这个block也会在那时返回,当blcok的代码执行完,它就出栈释放了。

这样的block中如果使用weak self,则在block返回之前self是可以释放的,毕竟是弱指针嘛。这样,在block返回时,因为self释放后为nil,而nil调用任何方法都会返回空,所以这样不会有什么问题。

至于在网络请求中什么时候用weak,什么时候不用weak,倒也是有技巧可循的,总结有以下两点,希望对你有所帮助:
1、如果你打算让页面dismiss或者pop后立马释放而不关心请求结果的处理,则应该再该页面请求的回调中使用weak self。
2、如果你希望页面在处理完请求的数据之后再释放,如该数据需要缓存等,则应该在该页面的请求的回调中使用self。


收尾

是时候来个真正的总结了:
1、只有作为property的block才可能导致循环引用。
2、作为函数参数的block中只能暂时强引用self,如果不是出于bug导致block参数被强引用不能释放,在这样的block中可以放心的使用self的强指针。
3、网络请求一类的block中,可以根据需求来决定是否使用slef的强指针。


下一篇博文我们将会讲一讲Objective-C开发过程中可能导致循环引用的例子(不仅仅时block哦),敬请期待。

以上观点均为个人研究、理解,如有不妥之处,欢迎指正,谢谢!

原创粉丝点击