处理循环引用 Retain Cycle

Mike Ash Friday Q&A 中文译文:处理循环引用 Retain Cycle

作者 TommyWu
封面圖片: 处理循环引用 Retain Cycle

译文 · 原文: Friday Q&A 2010-04-30: Dealing with Retain Cycles · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2010-04-30-dealing-with-retain-cycles.html 发布:2010-04-30 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


祝大家 iPad 3G 发布日快乐。无论你是在排队等购、等待快递,还是像我一样只能在家望眼欲穿,都可以用新一期的 Friday Q & A 来填补闲暇时光。本周,Filip van der Meeren 建议我讨论一下 retain cycles(保留环)以及如何处理它们。

保留环 首先我们需要明确 retain cycles 究竟是什么。我假设你已经熟悉标准的 Cocoa 内存管理。最简单的 retain cycle 就是两个对象互相持有对方:

Object A
| ^
| |
v |
Object B

保留环之所以是个问题,是因为 Cocoa 内存管理的标准是在对象的 dealloc 方法中释放此类被保留的引用。然而,如果一个对象正被保留,它就不会调用 dealloc。处于保留环中的对象将永远不会被释放,并且如果与应用程序的其他部分分离,就会发生内存泄漏。

需要注意的是,保留环可能涉及你自己的类,但也可能涉及 Cocoa 的类。Cocoa 类中导致保留环最常见的两个 “罪魁祸首” 是 NSTimerNSThread

还需注意,保留环仅影响使用手动内存管理(manual memory management)的代码。Cocoa 的垃圾收集器(garbage collector)能够检测并销毁那些互相持有强引用但外部没有引用的对象。然而,如果你使用的是 retain / release 内存管理(例如在 iPhone 开发时),保留环就是一个巨大的威胁;甚至在一个使用垃圾收集的应用程序中,如果你使用 CFRetainCFRelease 来绕过收集器,保留环也可能出现。

避免循环引用
苹果的内存管理准则指出,当两个对象存在父子关系时,父对象应持有(retain)子对象。若子对象需要反向引用父对象,该引用应为未持有的(unretained)、弱引用(weak reference)。这允许父对象被释放,进而解除对子对象的引用,避免形成循环。

然而,有时两个对象是对等关系—— 它们互不为父子,却需要相互引用。若这些引用是持有的,则会产生循环。

处理方式之一是将关系重构为父子关系。可任意选定一方作为父对象,使其持有对另一方的引用;另一方则对父对象使用弱引用。此时循环关系图可表示为:

Object A
| ^
| :
v :
Object B
- (void)dealloc
{
[_b setAReference: nil];
[_b release];
[super dealloc];
}

另一种方法是让另一个对象充当这两个子对象的父对象。这种方法既适用于子对象之间的强引用(retained references)关系,也适用于弱引用(weak references)关系。使用强引用时:

Object C
| |
| |
v v
Object A<====>Object B
- (void)dealloc
{
// break the cycle by zeroing the reference
[_a setBReference: nil];
// this breaks the cycle in both directions; this is optional
[_b setAReference: nil];
[_a release];
[_b release];
[super dealloc];
}
Object C
| |
| |
v v
Object A<::::>Object B

哪种方式更好?两者基本上是等价的。我认为使用保留引用(retained references)更安全一些,既在于如果你之后更改了对象图(object graph)它仍能继续正确工作,也在于它对代码中的错误更有抵抗力。

NSThread 和 NSTimer

NSThreadNSTimer 是导致循环引用(retain cycles)的常见原因。编写类似这样的代码并不少见:

- (id)init
{
...
_timer = [[NSTimer scheduledTimerWithTimeInterval: 0.1 target: self selector: @selector(whatever) userInfo: nil repeats: YES] retain];
...
}
- (void)dealloc
{
[_timer invalidate];
[_timer release];
[super dealloc];
}

同样的问题也会发生在 NSThread 上:当指定 self 为目标,然后在 dealloc 方法中关闭线程时,dealloc 方法永远不会被调用,导致线程永远无法关闭。

处理这个问题有两种方式:一种是强制要求显式失效(invalidation),另一种是将代码拆分为两个类。

NSTimer 不一定会在你释放最后一个引用时被销毁。只要计时器处于活动状态,运行循环(runloop)就会保持对它的引用。要销毁一个重复计时器,你不能仅仅释放所有引用,而必须显式地让它失效(invalidate)。

你可以为自己的类借鉴这一概念。只需暴露自己的失效(invalidate)方法,并用它来销毁计时器:

- (void)invalidate
{
[_timer invalidate];
[_timer release];
_timer = nil;
}

另一种方式是将代码拆分为两个类。你有一个暴露给外部的「外壳类」(shell class),它负责管理线程或计时器。然后还有一个「实现类」(implementation class),它作为线程或计时器的目标,完成大部分实际工作:

@implementation MyClassImpl
- (id)init
{
...
_timer = [[NSTimer scheduledTimerWithTimeInterval: 0.1 target: self selector: @selector(_timerAction) userInfo: nil repeats: YES] retain];
...
}
- (void)invalidate
{
[_timer invalidate];
[_timer release];
_timer = nil;
}
- (void)doThingy
{
// do stuff here
}
- (void)_timerAction
{
// periodic code here
}
@end
@implementation MyClass
- (id)init
{
...
_impl = [[MyClassImpl alloc] init];
...
}
- (void)dealloc
{
[_impl invalidate];
[_impl release];
[super dealloc];
}
- (void)doThingy
{
// just pass it on to the "real" code
[_impl doThingy];
}
@end

Blocks(块)

因为 blocks 会 retain(保留)它们引用的对象,所以它们也是导致 retain cycle(循环引用)的绝佳候选者。考虑以下代码:

- (id)init
{
...
_observerObj = [[NSNotificationCenter defaultCenter] addObserverForName: ... queue: [NSOperationQueue mainQueue] usingBlock: ^(NSNotification *note) {
[self doSomethingWith: note];
}];
[_observerObj retain];
...
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver: _observerObj];
[_observerObj release];
[super dealloc];
}

针对 NSTimer 和 NSThread 的解决方案在此同样适用:要么为该类的 API 添加明确的失效方法,要么将该类拆分为两个部分。

这里还有一个专用于 blocks 的解决方案,你可以使用它:即通过一个声明为 __block 的变量来引用 self,这样 self 将不会被强引用(retain)。

__block MyClass *blockSelf = self;
_observerObj = [[NSNotificationCenter defaultCenter] addObserverForName: ... queue: [NSOperationQueue mainQueue] usingBlock: ^(NSNotification *note) {
[blockSelf doSomethingWith: note];
}];

查找保留环
在大多数情况下,标准的内存泄漏检测技术可以有效发现导致泄漏的保留环(retain cycles)。Instruments(性能分析工具)是查找此类问题的良好途径,其 ObjectAlloc(对象分配追踪)和 Leaks(内存泄漏检测)工具均适用。若遇到难以分析的保留环,这些工具对每个对象的 retain(持有)和 release(释放)调用的追踪功能将大有帮助。

若你偏爱命令行工具,或需要更易于搜索的文本输出,leaks 命令行工具同样便捷。

需要注意的是,当保留环存在外部引用时,泄漏检测工具可能无法准确识别。例如,考虑一个涉及 NSTimer 的保留环:运行循环(runloop)引用了定时器,定时器又引用了你的对象,因此它们都处于可达状态。此时 Leaks 工具和 leaks 命令行工具均不会将其判定为泄漏。然而,若这些对象不执行任何功能却无限累积,那么即使它们在技术上可达,仍属于内存泄漏。ObjectAlloc 工具会显示这种累积现象,而其他工具可能无法识别此类泄漏。

Conclusion

Retain cycles(循环引用)是 Cocoa 内存管理系统上一个不幸的瑕疵。然而,只要稍加注意,就可以避免或修复它们,只需对对象层次结构进行小改动,就能最小化痛苦。特别注意 NSTimer 和 NSThread(但别忽略其他代码!),然后要么消除循环,要么添加明确打破循环的代码。

本周内容到此结束。七天后再来观看下一期的 Friday Q & A。一如既往,Friday Q & A 由读者投稿驱动。如果你有想要在此处看到的主题想法,请发送过来。


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2010-04-30-dealing-with-retain-cycles.html

Happy iPad 3G day to everyone. Whether you’re waiting in line, waiting for the delivery guy, or just pining at home like I am, you can fill your idle moments with another edition of Friday Q&A. This week, Filip van der Meeren has suggested that I discuss retain cycles and how to deal with them.

Retain Cycles First we need to discuss exactly what a retain cycle is. I’ll assume you’re familiar with standard Cocoa memory management. The simplest retain cycle is when two objects retain each other:

Object A
| ^
| |
v |
Object B

Retain cycles are a problem because the standard for Cocoa memory management is to release such retained references in an object’s dealloc method. However, if an object is being retained, it won’t dealloc. Objects which are part of a retain cycle will never be deallocated, and will leak if separated from the rest of the application.

Note that retain cycles can involve your own classes, but can also involve Cocoa classes. Two of the most common culprits in retain cycles in Cocoa classes are NSTimer and NSThread.

Also note that retain cycles only affect code that uses manual memory management. Cocoa’s garbage collector is able to detect and destroy objects which have strong references to each other but which aren’t referenced from the outside. However, retain cycles are a big threat if you’re using retain/release memory management (like if you’re on the iPhone), and can even show up in a garbage collected application if you use CFRetain and CFRelease to bypass the collector.

Avoiding Retain Cycles Apple’s memory management guidelines state that when two objects have a parent-child relationship, the parent should retain the child. If the child needs a reference back to the parent, that reference should be an unretained, weak reference. This allows the parent to be deallocated, which can then release its reference to the child, avoiding a cycle.

However, sometimes you have two objects which are peers. Neither one is a parent of the other, but they need to reference each other. If these references are retained, then you have a cycle.

One way to deal with this is to redefine the relationship into parent-child. You can arbitrarily choose one to be the parent object, which will have a retained reference to the other. The other can then have a weak reference to the first. The cycle diagram then looks like this:

Object A
| ^
| :
v :
Object B
- (void)dealloc
{
[_b setAReference: nil];
[_b release];
[super dealloc];
}

An alternative approach is to have another object act as the parent for both sub-objects. This can work with either retained or weak references between the sub-objects. With retained references:

Object C
| |
| |
v v
Object A<====>Object B
- (void)dealloc
{
// break the cycle by zeroing the reference
[_a setBReference: nil];
// this breaks the cycle in both directions; this is optional
[_b setAReference: nil];
[_a release];
[_b release];
[super dealloc];
}
Object C
| |
| |
v v
Object A<::::>Object B

Which way is better? They’re both basically equivalent. I think that using retained references is a bit safer, both in terms of continuing to work correctly if you change the object graph later, and in being more resistant to mistakes in the code.

NSThread and NSTimer NSThread and NSTimer are common causes of retain cycles. It’s not unusual to write code like this:

- (id)init
{
...
_timer = [[NSTimer scheduledTimerWithTimeInterval: 0.1 target: self selector: @selector(whatever) userInfo: nil repeats: YES] retain];
...
}
- (void)dealloc
{
[_timer invalidate];
[_timer release];
[super dealloc];
}

This exact same problem also happens with an NSThread, when specifying self as the target, and then shutting down the thread in dealloc. The dealloc method will never run, so the thread will never be shut down.

There are two ways to deal with this problem. One is to force explicit invalidation, and one is to split your code into two classes.

An NSTimer won’t necessarily be destroyed when you release your final reference to it. As long as the timer is active, the runloop keeps a reference to it. To destroy a repeating timer, you can’t just release all of your references to it, you have to explicitly invalidate it.

You can borrow this concept for your own class. Just expose your own invalidate method, and use that to destroy the timer:

- (void)invalidate
{
[_timer invalidate];
[_timer release];
_timer = nil;
}

The other way is to split your code into two classes. You have a shell class which is exposed to the outside world, and which manages the thread or timer. Then you have an implementation class which is the target of the thread or timer, and which does most of the actual work:

@implementation MyClassImpl
- (id)init
{
...
_timer = [[NSTimer scheduledTimerWithTimeInterval: 0.1 target: self selector: @selector(_timerAction) userInfo: nil repeats: YES] retain];
...
}
- (void)invalidate
{
[_timer invalidate];
[_timer release];
_timer = nil;
}
- (void)doThingy
{
// do stuff here
}
- (void)_timerAction
{
// periodic code here
}
@end
@implementation MyClass
- (id)init
{
...
_impl = [[MyClassImpl alloc] init];
...
}
- (void)dealloc
{
[_impl invalidate];
[_impl release];
[super dealloc];
}
- (void)doThingy
{
// just pass it on to the "real" code
[_impl doThingy];
}
@end

Blocks Because blocks retain the objects they reference, they’re another excellent candidate for a retain cycle. Consider this code:

- (id)init
{
...
_observerObj = [[NSNotificationCenter defaultCenter] addObserverForName: ... queue: [NSOperationQueue mainQueue] usingBlock: ^(NSNotification *note) {
[self doSomethingWith: note];
}];
[_observerObj retain];
...
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver: _observerObj];
[_observerObj release];
[super dealloc];
}

The solutions used for NSTimer and NSThread will work here as well: either add explicit invalidation to the class’s API, or break the class into two pieces.

There’s a blocks-specific solution that you can use as well, which is to refer to self through a variable declared __block, which will not be retained:

__block MyClass *blockSelf = self;
_observerObj = [[NSNotificationCenter defaultCenter] addObserverForName: ... queue: [NSOperationQueue mainQueue] usingBlock: ^(NSNotification *note) {
[blockSelf doSomethingWith: note];
}];

Finding Cycles For the most part, standard leak finding techniques will work fine for finding retain cycles that cause a leak. Instruments is a good way to find them, both the ObjectAlloc instrument and the Leaks instrument. If you have a cycle that’s hard to figure out, its ability to track retain and release calls to each object can help a lot.

If you prefer the command line, or just need text that’s easier to search through, the leaks command-line tool is also handy.

When hunting for cycles, note that leaks tools won’t always find a cycle if there’s an external reference into the cycle. For example, consider a cycle involving an NSTimer. There’s a reference from the runloop to the timer, and from the timer to your object, so they’re both reachable. Both the Leaks instrument and the leaks tool will not consider this to be a leak. However, if they’re doing nothing and build up without end, then it still is a leak, even if they’re technically reachable. The ObjectAlloc tool will show this buildup even though the other tools won’t identify the leak.

Conclusion Retain cycles are an unfortunate wart on Cocoa’s memory management system. However, with some care, they can be avoided or fixed with a minimum of pain, with small changes to your object hierarchy. Pay extra attention to NSTimer and NSThread (but don’t ignore other code!), then either eliminate the cycle or add code that explicitly breaks it.

That’s it for this week. Come back in another seven days for the next Friday Q&A. As always, Friday Q&A is driven by reader submissions. If you have an idea for a topic that you’d like to see covered here, please send it in.