本文由 伯乐在线 - 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 下的循环引用的更多相关文章

  1. block本质探寻八之循环引用

    说明:阅读本文,请参照之前的block文章加以理解: 一.循环引用的本质 //代码——ARC环境 void test1() { Person *per = [[Person alloc] init]; ...

  2. iOS 循环引用 委托 (实例说明)

    如何避免循环引用造成的内存泄漏呢: 以delegate模式为例(viewcontroller和view之间就是代理模式,viewcontroller有view的使用权,viewcontroller同时 ...

  3. ARC下的block导致的循环引用问题解析

    ARC下的block导致的循环引用问题解析 更详细细节请参考 http://blog.sina.com.cn/s/blog_8c87ba3b0101m599.html ARC下,copy到堆上的blo ...

  4. ARC下循环引用的问题

    最初 最近在开发应用时碰到使用ASIHttpRequest后在某些机器上发不出请求的问题,项目开启了ARC,代码是这样写的: @implement MainController - (void) fe ...

  5. iOS ARC下循环引用的问题 -举例说明strong和weak的区别

    strong:适用于OC对象,作用和非ARC中的retain作用相同,它修饰的成员变量为强指针类型weak:适用于OC对象,作用和非ARC中的assign作用相同,修饰的成员变量为弱指针类型assig ...

  6. block使用小结、在arc中使用block、如何防止循环引用

    引言 使用block已经有一段时间了,感觉自己了解的还行,但是几天前看到CocoaChina上一个关于block的小测试主题: [小测试]你真的知道blocks在Objective-C中是怎么工作的吗 ...

  7. 0c-41-ARC下循环引用问题

    1.ARC下循环引入问题 一个人拥有一只狗,一只狗拥有一个主人. 当增加d.owner = p;时形成循环引用. 解决方法:一端用strong,一端用weak. 2.ARC下@property参数 A ...

  8. 深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用(下)

    深入研究Block捕获外部变量和__block实现原理 EOCNetworkFetcher.h typedef void (^EOCNetworkFetcherCompletionHandler)(N ...

  9. Spring的3级缓存和循环引用的理解

    此处是我自己的一个理解,防止以后忘记,如若那个地方理解不对,欢迎指出. 一.背景 在我们写代码的过程中一般会使用 @Autowired 来注入另外的一个对象,但有些时候发生了 循环依赖,但是我们的代码 ...

随机推荐

  1. 绝对定位多个字居中显示的css

    在工作中遇到一种情况,例如把一个div元素绝对定位到一个位置,但是该元素中的文字个数不确定,还要保证始终该文字是居中显示,则可以定义两个div,外层div绝对定位并加一个宽度,内层div居中 .box ...

  2. Linux 目录下属性查看操作

    1. 查看当前目录所有文件和文件夹的大小 方法一: $du -sh * 或 $du -h -d 0 * '-d 0' 代表查询目录的深度为0 ,也就是当前目录,'-d 3' 表示文件目录深度为3,可以 ...

  3. Node.js 模块之 morgan中间件记录日志

    NodeJs中Express框架使用morgan中间件记录日志 Express中的app.js文件已经默认引入了该中间件var logger = require('morgan'); 使用app.us ...

  4. Unity接第三方SDK时遇到的坑

    1.大部分SDK的方法需要在线程中执行,一般会放在主线程里执行,安卓中主线程一般用于UI渲染. this.runOnUiThread(new Runnable() { @Override public ...

  5. Python中使用MongoEngine

    pymongo来操作MongoDB数据库,但是直接把对于数据库的操作代码都写在脚本中,这会让应用的代码耦合性太强,而且不利于代码的优化管理 一般应用都是使用MVC框架来设计的,为了更好地维持MVC结构 ...

  6. js九九乘法表的应用

    <html> <head> <meta charset=utf-8" /> <title>js九九乘法表</title> < ...

  7. oracle 产生随机数

    -- 产生一个任意大小的随机数select dbms_random.random from dual; -- 产生一个100以内的随机数select abs(mod(dbms_random.rando ...

  8. 开发kendo-ui弹窗组件

    摘要: kendo-ui中只是提供了windwo插件,并没有提供页内弹窗插件.现在分享项目中自己定制的基于window组件的弹窗插件,如果你的项目也是用的kendo-ui,只需要将组件代码引到项目中即 ...

  9. redis sentinels哨兵集群环境配置

    # Redis configuration file example. # # Note that in order to read the configuration file, Redis mus ...

  10. python--文件I/O--11

    原创博文,转载请标明出处--周学伟http://www.cnblogs.com/zxouxuewei/ 本章只讲述所有基本的的I/O函数,更多函数请参考Python标准文档. 一.打印到屏幕 最简单的 ...