理解 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 视图会显示关于泄露的栈追踪,甚至可以直接定位到出问题的代码。

时间: 2024-10-15 16:40:28

理解 ARC 下的循环引用的相关文章

和block循环引用说再见

to be block? or to be delegate? 这是一个钻石恒久远的问题.个人在编码中暂时没有发现两者不能通用的地方,习惯上更偏向于block,没有什么很深刻的原因,只是认为block回调写起来更便捷,直接在上下文中写block回调使得代码结构更清晰,可读性更强.而delegate还需要申明protocol接口,设置代理对象,回调方法与上下文环境不能很好契合,维护起来没有block方便.另外初学者很容易会被忘记设置代理对象坑- 然而惯用block是有代价的,最大的风险就是循环引用

block本质探寻八之循环引用

说明:阅读本文,请参照之前的block文章加以理解: 一.循环引用的本质 //代码——ARC环境 void test1() { Person *per = [[Person alloc] init]; per.age = 10; per.block = ^{ NSLog(@"-------1"); }; } int main(int argc, const char * argv[]) { @autoreleasepool { test1(); // test2(); } NSLog(

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

如何避免循环引用造成的内存泄漏呢: 以delegate模式为例(viewcontroller和view之间就是代理模式,viewcontroller有view的使用权,viewcontroller同时也是view的代理(处理view中的事件)): UserWebService.h #import //定义一个ws完成的delegate @protocol WsCompleteDelegate @required -(void) finished;//需要实现的方法 @end @interface

ARC下循环引用的问题

最初 最近在开发应用时碰到使用ASIHttpRequest后在某些机器上发不出请求的问题,项目开启了ARC,代码是这样写的: 1 2 3 4 5 6 7 8 9 @implement MainController - (void) fetchUrl{     ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];     [request setCompletionBlock

[转]iOS中ARC下Block的循环引用

[ARC的特性] ARC下,所有NSObject类型指针, 1. 默认为__strong类型 2. 可以显示的指定为__weak类型,__weak类型指针在所指向对象销毁后会自动置为nil 3. __autorelesing类型用于inout参数类型 ARC下,当一个函数返回一个NSObject指针时,编译器会帮我们实现autorelease调用.例如: return pObject; 编译器会帮我们扩展为 return [pObject autorelease]; ARC下,不能显式relea

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

引言 使用block已经有一段时间了,感觉自己了解的还行,但是几天前看到CocoaChina上一个关于block的小测试主题 : [小测试]你真的知道blocks在Objective-C中是怎么工作的吗?,发现竟然做错了几道, 才知道自己想当然的理解是错误的,所以抽时间学习了下,并且通过一些测试代码进行测试,产生这篇博客. Block简介(copy一段) Block作为C语言的扩展,并不是高新技术,和其他语言的闭包或lambda表达式是一回事.需要注意的是由于Objective-C在iOS中不支

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

深入研究Block捕获外部变量和__block实现原理 EOCNetworkFetcher.h typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data); @interface EOCNetworkFetcher : NSObject @property (nonatomic, strong, readonly) NSURL *url; - (id)initWithURL:(NSURL *)url; - (void)star

Spring 循环引用 ——理解singleton与prototype初始化的区别

所谓的循环引用,就是A依赖B,B又依赖A,A与B两个对象相互持有.像下面这种情况: class A { B b; public A(B b) { this.b=b; } } class B { A a; public B(A a ) { this.a=a; } } 我们知道spring在获取对象或者在加载的时候,触发依赖注入.例如触发A对象的依赖注入,发现需要B对象,而此时B还没有初始化,就去实例化B对象,而又需要A对象,这样就进入了一种死循环状态,有点像操作系统里面的死锁.似乎这种情况发生了,

ARC下的引用计数----debug

OBJC_EXTERN int _objc_rootRetainCount(id); _objc_rootRetainCount(self.QRcodeVc); NSLog(@"_objc_rootRetainCount(qr)%d",_objc_rootRetainCount(self.QRcodeVc)); 由于ARC是系统管理内存,虽然省却了大部分的内存管理,但是一旦循环引用集很难排除,这里是ARC下debug模式打印对象的引用计数,只能是debug!