ARC 下的循环引用相似于日本的 B 级恐怖片。当你刚成为苹果开发者,你或许不会关心他们的存在。直到某天你的一个 app 因内存泄露而闪退,你才忽然意识到他们的存在,而且发现循环引用像幽灵同样存在于代码的各个角落。年复一年,你开始学会如何处理循环引用,检测和避免它们,可是这部片子的恐怖结局仍是在那里,随时可能出现。web
ARC 令许多开发者(包括我)感到失望的地方之一是苹果保留了用 ARC 来进行内存管理。ARC 很不幸地没有包括一个循环引用检测器,因此很容易就会产生循环引用,所以迫使开发者在写代码的时候采起一些特别的防范措施。objective-c
循环引用一直是一些 iOS 开发者感到费解的一个问题。 网上有许多误导信息[1][2],这些文章给了错误的建议和修复方法,其方法甚至可能引起问题和致使 app 闪退。在这片文章,我想要针对这些问题解释清楚。编程
内存管理能够追溯到手动内存管理(Manual Retain Release,简称 MRR)。在 MRR,开发者建立的每个对象,须要声明其拥有权,从而保持对象存在于内存中,当对象再也不须要的时候撤销拥有权释放它。MRR 经过引用计数系统实现这套拥有权体系,也就是说每一个对象有个计数器,经过计数加1代表被一个对象拥有,减1代表再也不持有。当计数为零,对象将被释放。因为手动管理内存实在太烦人,所以苹果推出了自动引用计数(ARC)来解放开发者,再也不须要开发者手动添加 retain 和 release 操做,从而能够专一于 App 开发。在 ARC,开发者将会定义一个变量为“strong”或“weak”。一个 weak 弱引用没法 retain 对象,而 strong 引用会 retain 这个对象,并将其引用计数加一。swift
ARC 的问题是循环引用很容易发生。当两个不一样的对象各有一个强引用指向对方,那么循环引用便产生了。试想下,一个 book 对象持有多个 page 对象,每一个 page 对象又有个属性指向它所属的 book 对象。当你释放了持有 book 和 page 对象的变量时,他们仍然还有强引用指向各自,所以你没法释放他们的内存,即便已经没有变量持有他们。安全
不幸的是,循环引用在实际中并无那么容易被发现。多个对象之间(A 持有 B,B 持有 C,C 也刚好持有 A)也能够产生循环引用。更糟的是,Objective-C block 和 Swift 闭包都是独立内存对象,它们会持有其所引用的对象,因而就引起了潜在的循环引用问题。闭包
循环引用对 app 有潜在的危害,会使内存消耗太高,性能变差和 app 闪退等。然而,苹果文档对于可能发生循环引用的场景以及如何避免并无详细描述,这就容易致使一些误解和不良的编程习惯。app
废话很少说,咱们一块儿来分析一些场景中是否会产生循环引用,以及如何避免它。async
父子对象关系是一个循环引用的典型案例,不幸的是,它也是惟一一个存在于苹果文档中的案例。其实就是前文描述的 Book 与 Page 案例。典型的解决方法就是,在子类定义一个指向父类的变量,声明为 weak 弱引用,从而避免循环引用。wordpress
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 都是独立的内存对象,会 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 是个可选值。
和咱们一般所认为的不一样,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 若是没有引用任何实例或类变量,其自己也不会形成循环引用。最多见的一个例子就是 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?
在大多数的状况中,一个对象的代理持有一个实例化的对象,或应当生命周期长于该对象(从而响应代理方法),所以一个设计良好的类应该不须要咱们考虑任何有关生命周期的问题。
无论我多努力仔细,我有时仍是会忘记声明一个弱引用,而后意外地建立一个新的对象(感谢 ARC 的无所做为!)。幸运的是,XCode 自带了一个很强大的工具 Instruments,用于检测和定位循环引用。一旦你的 app 开发结束,即将提交到 Apple Store,先分析你的 app 是一个好的习惯。Instruments 有不少组件,能够用来分析 app 的不一样方面,可是咱们如今关心的时 Leak 选项。
Instruments 一启动,你的应用也应该启动了,而后执行一些交互操做,特别是你想要测试的区域或视图控制器。被检测到的泄露都会以一条红色线显示在 Leaks 区域。Assistant 视图会显示关于泄露的栈追踪,甚至能够直接定位到出问题的代码。