理解 ARC 下的循环引用
本文由 伯乐在线 - nathanw 翻译,dopcn 校稿。未经许可,禁止转载!
英文出处:digitalleaves.com。欢迎加入翻译组。

ARC 下的循环引用类似于日本的 B 级恐怖片。当你刚成为苹果开发者,你或许不会关心他们的存在。直到某天你的一个 app 因内存泄露而闪退,你才突然意识到他们的存在,并且发现循环引用像幽灵一样存在于代码的各个角落。年复一年,你开始学会如何处理循环引用,检测和避免它们,但是这部片子的恐怖结局还是在那里,随时可能出现。
ARC 令许多开发者(包括我)感到失望的地方之一是苹果保留了用 ARC 来进行内存管理。ARC 很不幸地没有包括一个循环引用检测器,所以很容易就会产生循环引用,因此迫使开发者在写代码的时候采取一些特别的防范措施。
循环引用一直是一些 iOS 开发者感到费解的一个问题。 网上有许多误导信息[1][2],这些文章给了错误的建议和修复方法,其方法甚至可能引发问题和导致 app 闪退。在这片文章,我想要针对这些问题解释清楚。
理论简介
内存管理可以追溯到手动内存管理(Manual Retain Release,简称 MRR)。在 MRR,开发者创建的每一个对象,需要声明其拥有权,从而保持对象存在于内存中,当对象不再需要的时候撤销拥有权释放它。MRR 通过引用计数系统实现这套拥有权体系,也就是说每个对象有个计数器,通过计数加1表明被一个对象拥有,减1表明不再持有。当计数为零,对象将被释放。由于手动管理内存实在太烦人,因此苹果推出了自动引用计数(ARC)来解放开发者,不再需要开发者手动添加 retain 和 release 操作,从而可以专注于 App 开发。在 ARC,开发者将会定义一个变量为“strong”或“weak”。一个 weak 弱引用无法 retain 对象,而 strong 引用会 retain 这个对象,并将其引用计数加一。

我为什么要关心这些?
ARC 的问题是循环引用很容易发生。当两个不同的对象各有一个强引用指向对方,那么循环引用便产生了。试想下,一个 book 对象持有多个 page 对象,每个 page 对象又有个属性指向它所属的 book 对象。当你释放了持有 book 和 page 对象的变量时,他们仍然还有强引用指向各自,因此你无法释放他们的内存,即使已经没有变量持有他们。
不幸的是,循环引用在实际中并没有那么容易被发现。多个对象之间(A 持有 B,B 持有 C,C 也恰好持有 A)也可以产生循环引用。更糟的是,Objective-C block 和 Swift 闭包都是独立内存对象,它们会持有其所引用的对象,于是就引发了潜在的循环引用问题。
循环引用对 app 有潜在的危害,会使内存消耗过高,性能变差和 app 闪退等。然而,苹果文档对于可能发生循环引用的场景以及如何避免并没有详细描述,这就容易导致一些误解和不良的编程习惯。
一些用例模拟
废话不多说,我们一起来分析一些场景中是否会产生循环引用,以及如何避免它。
父子对象关系
父子对象关系是一个循环引用的典型案例,不幸的是,它也是唯一一个存在于苹果文档中的案例。其实就是前文描述的 Book 与 Page 案例。典型的解决方法就是,在子类定义一个指向父类的变量,声明为 weak 弱引用,从而避免循环引用。
Objective-C
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class Parent {
var name: String
var child: Child?
init(name: String) {
self.name = name
}
}
class Child {
var name: String
weak var parent: Parent!
init(name: String, parent: Parent) {
self.name = name
self.parent = parent
}
}
|
在 swift 中子类指向父对象的变量是一个弱引用,这就迫使我们将该弱引用定义为 optional 类型。如果不使用 optional 可以有另一种做法,将指向父对象的变量声明为“无主引用(unowned)”(表明我们不持有该对象,也不对其进行内存管理)。然而在这种情况下,我们必须非常小心,确保只要还有子对象指向它,父对象不变成 nil,否则会直接闪退。
Objective-C
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class Parent {
var name: String
var child: Child?
init(name: String) {
self.name = name
}
}
class Child {
var name: String
unowned var parent: Parent
init(name: String, parent: Parent) {
self.name = name
self.parent = parent
}
}
var parent: Parent! = Parent(name: "John")
var child: Child! = Child(name: "Alan", parent: parent)
parent = nil
child.parent <== possible crash here!
|
通常有效的做法是,父对象必须持有(强引用)子对象,而子对象只要保持一个弱引用指向他们的父对象。这同样适用于集合对象,它们必须持有它们包含的对象。
当 Block 和闭包包含在类的成员变量中
另外一个典型的例子,可能不是那么直观。如我们前面解释的,闭包和 block 都是独立的内存对象,会 retain 它们所引用的对象,因此如果我们有个类,里面有个闭包变量,并且这个闭包恰好引用了自身所属对象的一个属性或方法,那么就可能产生循环引用,因为闭包会创建强引用捕获“self”。
Objective-C
|
1
2
3
4
5
|
class MyClass {
lazy var myClosureVar = {
self.doSomething()
}
}
|
这个案例的解决方法是定义一个弱版本的 self,然后在闭包或 block 中使用。在 objective-C,我们会定义一个新的变量:
Objective-C
|
1
2
3
4
5
6
|
class="lang-objc">- (id) init() {
__weak MyClass * weakSelf = self;
self.myClosureVar = ^{
[weakSelf doSomething];
}
}
|
然而在 Swift 我们只需要在闭包的头部声明 “[weak self in]”:
Objective-C
|
1
2
3
4
|
var myClosureVar = {
[weak self] in
self?.doSomething()
}
|
用这个方法,当闭包结束的时候,内部的 self 变量不会被强引用,所以它会被释放,打破了循环引用。注意当 self 被声明为 weak,闭包内部的 self 是个可选值。
GCD: dispatch_async
和我们通常所认为的不同,dispatch_async 自身不会造成循环引用
Objective-C
|
1
2
3
|
dispatch_async(queue, { () -> Void in
self.doSomething();
});
|
在这里,闭包会强引用 self,但是实例化的 self 不会强引用闭包,所以一旦闭包结束,它就会被释放,所以循环引用也不会产生。然而,总有些开发者认为它可能会产生循环引用。有些开发者甚至以为,所有在 block 和闭包里面的 self 都需要弱引用:
Objective-C
|
1
2
3
4
|
dispatch_async(queue, {
[weak self] in
self?.doSomething()
})
|
在我看来,每种情况都采用这种方法并不是一个好的实践。让我们试想下,如果我们有个对象,用于发送一个后台任务(比如下载数据),并且调用了 self 的一个方法。这时如果我们弱引用 self,该对象的生命周期结束早于闭包结束被释放,因而当我们的闭包调用的 doSomething()方法,该对象可能就不存在了,方法也得不到执行。合适的解决方法是(苹果推荐)在闭包内部,声明一个强引用指向弱引用。
Objective-C
|
1
2
3
4
5
6
|
dispatch_async(queue, {
[weak self] in
if let strongSelf = self {
strongSelf.doSomething()
}
})
|
我觉得这种语法不仅恶心乏味不直观,而且违反了闭包作为一个独立处理实体的原则。学会理解对象的生命周期,明白何时应该声明弱引用,以及对象生存周期的意义,这很重要。但是,这又使得我分心而无法专注于 app 开发的问题本身,如果 Cocoa 不使用 ARC,也就不必要写这些代码。
本地闭包和 block
函数的闭包和 block 如果没有引用任何实例或类变量,其本身也不会造成循环引用。最常见的一个例子就是 UIView 的 animateWithDuration。
Objective-C
|
1
2
3
4
5
6
7
|
func myMethod() {
...
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.someOutlet.alpha = 1.0
self.someMethod()
})
}
|
和 dispatch_async 和其他相关的 GCD 相关方法一样,我们不需要担心局部变量闭包和 block 产生循环引用。
代理协议
代理协议也是一个典型的场景,需要你使用弱引用来避免循环引用。将代理声明为 weak 是一个即好又安全的做法:
@property (nonatomic, weak) id <MyCustomDelegate> delegate;
在 swift:
weak var delegate: MyCustomDelegate?
在大多数的情况中,一个对象的代理持有一个实例化的对象,或应当生命周期长于该对象(从而响应代理方法),因此一个设计良好的类应该不需要我们考虑任何有关生命周期的问题。
使用 Instruments 调试循环引用
不管我多努力仔细,我有时还是会忘记声明一个弱引用,然后意外地创建一个新的对象(感谢 ARC 的无所作为!)。幸运的是,XCode 自带了一个很强大的工具 Instruments,用于检测和定位循环引用。一旦你的 app 开发结束,即将提交到 Apple Store,先分析你的 app 是一个好的习惯。Instruments 有很多组件,可以用来分析 app 的不同方面,但是我们现在关心的时 Leak 选项。

Instruments 一启动,你的应用也应该启动了,然后执行一些交互操作,特别是你想要测试的区域或视图控制器。被检测到的泄露都会以一条红色线显示在 Leaks 区域。Assistant 视图会显示关于泄露的栈追踪,甚至可以直接定位到出问题的代码。

理解 ARC 下的循环引用的更多相关文章
- block本质探寻八之循环引用
说明:阅读本文,请参照之前的block文章加以理解: 一.循环引用的本质 //代码——ARC环境 void test1() { Person *per = [[Person alloc] init]; ...
- iOS 循环引用 委托 (实例说明)
如何避免循环引用造成的内存泄漏呢: 以delegate模式为例(viewcontroller和view之间就是代理模式,viewcontroller有view的使用权,viewcontroller同时 ...
- ARC下的block导致的循环引用问题解析
ARC下的block导致的循环引用问题解析 更详细细节请参考 http://blog.sina.com.cn/s/blog_8c87ba3b0101m599.html ARC下,copy到堆上的blo ...
- ARC下循环引用的问题
最初 最近在开发应用时碰到使用ASIHttpRequest后在某些机器上发不出请求的问题,项目开启了ARC,代码是这样写的: @implement MainController - (void) fe ...
- iOS ARC下循环引用的问题 -举例说明strong和weak的区别
strong:适用于OC对象,作用和非ARC中的retain作用相同,它修饰的成员变量为强指针类型weak:适用于OC对象,作用和非ARC中的assign作用相同,修饰的成员变量为弱指针类型assig ...
- block使用小结、在arc中使用block、如何防止循环引用
引言 使用block已经有一段时间了,感觉自己了解的还行,但是几天前看到CocoaChina上一个关于block的小测试主题: [小测试]你真的知道blocks在Objective-C中是怎么工作的吗 ...
- 0c-41-ARC下循环引用问题
1.ARC下循环引入问题 一个人拥有一只狗,一只狗拥有一个主人. 当增加d.owner = p;时形成循环引用. 解决方法:一端用strong,一端用weak. 2.ARC下@property参数 A ...
- 深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用(下)
深入研究Block捕获外部变量和__block实现原理 EOCNetworkFetcher.h typedef void (^EOCNetworkFetcherCompletionHandler)(N ...
- Spring的3级缓存和循环引用的理解
此处是我自己的一个理解,防止以后忘记,如若那个地方理解不对,欢迎指出. 一.背景 在我们写代码的过程中一般会使用 @Autowired 来注入另外的一个对象,但有些时候发生了 循环依赖,但是我们的代码 ...
随机推荐
- VCL 中的 Windows API 函数(4): AdjustWindowRectEx
AdjustWindowRectEx 用在了 Forms.DBCtrls 单元. AdjustWindowRectEx 可以根据窗口样式获取的边缘尺寸. 测试: var R: TRect; beg ...
- Android开发学习笔记-自定义对话框
系统默认的对话框只能显示简单的标题内容以及按钮,而如果想要多现实其他内容则就需要自定义对话框,下面是自定义对话框的方法. 1.先定义对话框的模版 <?xml version="1.0& ...
- JavaScript 词法、静态、动态作用域初级理解
开始之前 由于本人也是JavaScript初学者,记录学习经过,怕以后会忘记. 对于JavaScript 初学者来说,最难的不是代码部分,而是对很多书籍中的术语的理解,大多时候想要理解一段JavaSc ...
- 关于Android中Animation的停止【转载】
转载自:http://blog.csdn.net/easonx1990/article/details/8231520 最近遇到一个需求,通过在GridView上改变焦点,并且GridView上每个i ...
- uploadify在火狐下上传不了的解决方式,java版(Spring+SpringMVC+MyBatis)具体解决方式
因为技术选型的原因,在一个产品中.我选择了uploadify,选择它的原因是它有完好的技术文档说明(http://www.uploadify.com/documentation/),唯一不足的是 ...
- Netty权威指南之BIO(Block Input/Output,同步阻塞I/O通信)通信模型
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建 ...
- Bypass ngx_lua_waf SQL注入防御(多姿势)
0x00 前言 ngx_lua_waf是一款基于ngx_lua的web应用防火墙,使用简单,高性能.轻量级.默认防御规则在wafconf目录中,摘录几条核心的SQL注入防御规则: select.+ ...
- Redis 操作列表数据
Redis 操作列表数据: > lpush list1 "aaa" // lpush 用于追加列表元素,默认追加到列表的最左侧(left) (integer) > lp ...
- (原创)Windows下使用android ADT工具dmtracedump.exe绘图
在windows下使用dmtracedump绘图时,出现如下错误: 'dot' 不是内部或外部命令,也不是可运行的程序 或批处理文件. 应该是没有dot这个执行程序,安装:Graphviz程序,然后将 ...
- No.3 PyQt学习
使用box布局,写了 一个系统的主页(非常丑) 代码如下: # -*- coding: utf-8 -*- import sys from PyQt4.QtGui import * from PyQt ...