Next article: Another Week Without Friday Q&A
Previous article: Friday Q&A 2010-04-23: Implementing a Custom Slider
Tags: cocoa fridayqna memory release retain
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];
}
NSTableView
data sources.)
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];
}
_timer
. The run loop will also retain the timer, and won't release it untill the call to invalidate
. This acts as a second retained reference to the timer, causing what is essentially a cycle even without the explicit retained reference.
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
MyClass
becomes the common parent object, with MyClassImpl
and NSTimer
as the sub-objects. The parent then manually breaks the retain cycle between the sub-objects when it's destroyed. Externally, the parent preserves the normal retain/release semantics, with no need for explicit invalidation.
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];
}
self
, the block will retain self
. The result is a subtle retain cycle. This can happen even if you don't directly reference self
; simply referencing an instance variable will indirectly reference self
, which will cause the block to retain it.
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];
}];
blockSelf
is not retained. Be careful if you do this to avoid referring to instance variables directly, as those will still reference the original self
. If you need to access an instance variable, explicitly indirect through blockSelf
by doing blockSelf->_someIvar
.
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.
Comments:
http://www.mikeash.com/pyblog/friday-qa-2009-11-27-using-accessors-in-init-and-dealloc.html
For example, if I have, in a viewDidLoad:
myThread = [NSThread detachNewThreadSelector: @selector(go) toTarge:self withObject:nil];
And then I have
- (void) go
{
//do something
}
There is no cycle here, correct? When the go function ends, so does the Thread... or not?
What if the go function were something like this:
- (void) go
{
while (![NSThread isCancelled])
{
//alloc something
NSData *d = [NSData dataWithContentsOfFile: @"somefilepath"];
[NSThread sleepForTimeInterval:1];
}
}
I found out, in this case, that "d" is never deallocated, until the thread exits.
It seems that an @autoreleasepool{} is needed around the NSData allocation, for it to be properly released after each cycle. Is that correct?
But still, if I have for example:
- (IBAction) goBack
{
[myThread cancel];
myThread = nil;
[self.navigationController popViewControllerAnimated:YES];
}
This will still clear the thread, and the viewController itself, and all the NSData that were allocated, right?
Anything else I should be aware when doing things like these?
Thanks a lot for your very informative posts.
All Cocoa APIs need a periodic autorelease pool drain to clean up their stuff. If you're running your own long-lived thread, then you need an autorelease pool in your thread's top-level loop.
Comments RSS feed for this page
Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.