http://sealedace.com/blog/2014/01/23/block-retain-cycle/
在进入正题之前,建议读一下船哥的文章——iOS中消息的传递机制。里面除了描述iOS中消息的传递机制,还提供了一些思路帮助大家正确选择某种传递方法来完成开发工作。而本篇文章的主题是跟block相关的。
我们知道在使用block时,必须避免出现retain cycle
。如果写代码不仔细造成了retain cycle
,就会出现内存泄露。即使Xcode有静态代码分析工具,但很多时候Xcode也不太靠谱,根本什么提示都没有,所以还是自己写代码多注意比较好。
(下文译自The Correct Way to Avoid Capturing Self in Blocks With ARC,有小改动)
以前避免retain cycle
的方法大概像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
所有在block中被访问的变量都会被block保留,而上面这段代码的避免了对self
的直接引用。如果self持有这个block——直接或者间接,都会导致retain cycle
。因此,我们使用了__block
关键字来创建一个对self
的临时引用(非保留),我们在block会使用这个引用代替self
来操纵对象。但是很不幸,ARC出现后废弃了这种方法并且将__block
作用改成了跟strong
关键字一样。所以,我们需要一个新的方法来解决这个问题。
一个常见的(naive)方法就是像下面的代码一样去创建一个weak
引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
其实,这个跟之前的那个方法差不多是一样的,只是关键字变成了__weak
。用这个引用放在block里面运行,没有retain cycle
。但是不管怎样,这样做仍然有一些问题。
问题是,当self
在block执行期间或block执行之前被销毁了,会怎样呢?我们乍一看应该是没什么问题的:因为blockSelf
是一个weak
引用,它在销毁后会自动置空,而且对nil发送消息是不会发生任何情况的。对吧?
嗯,如果block中的代码就这样简单,没有复杂的消息传递就结束了,也许倒也没什么问题。但是,假如self
在这个block执行的期间,在其他线程里面被销毁了呢?假如在你block里面工作完成到一半的时候self
就释放置nil了呢?难道你就这样放弃block中要做的事情,就这样丢着不管了吗?是啊!你这样做也许没有什么损失。呵呵,但是这里还真有一种更加糟糕情况潜伏着…
请看下面这一段看起来感觉跟之前差不多的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
可以看到,这里我们是直接访问成员变量(很糟糕的方式),而没有通过Objective-C里面的推荐的成员访问器。这样写仍然会先访问到self
,然后才能访问它的成员变量。如果你这样做的话,runtime没有使用成员访问器,而是使用了C级别的空指针引用。换种方式来看,上面的代码会变成大概下面这个样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
所以,如果self
在block执行的途中被销毁了,呵呵,你就会遇到一个因为使用了空指针来访问成员变量而导致的crash。这种情况,跟对nil发送消息可不一样,因为你根本就是在对一个不存在的东西发送消息。
鉴于这些问题,最安全的做法是在block外部对self
先获取一个weak
引用,然在block内部执行代码的开始,创建一个对self
的strong
引用——这样在block执行期间,你不用担心self
被销毁。然后等你不需要用到self
的strong
引用时候,可以将strongSelf
置nil,或者干脆不管,等待代码执行结束退出(ARC,你懂的)。如果你按照这个方法做了,应该不会有什么问题了。所以,代码结构大致应该是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
另外,澄清一点,这段代码并不能保证线程安全,你仍然需要添加@synchronize
域或者线程锁来保证线程安全。不过可以肯定的是,这样写一定比文章中提到的存在问题的代码要安全的多!
经验小结
在用block的过程中,不要只对看得到self
加weak
引用,如果你像文章里那样直接访问成员变量,也是同样会存在retain cycle
的问题。解决问题办法也很简单,使用文章里提到的解决方案,或者干脆把大段的代码移出block,这样反而更好些。
欢迎大家纠错(-0_0)