Manual Retain Release
|
This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
|
ARC is the default and the right choice for all new code. This page exists so you can read and maintain pre-2011 Objective-C, understand what ARC automates, and recognise the bug patterns that still appear at the Core Foundation boundary. For how to actually manage memory today, go to Automatic Reference Counting. |
The Ownership Model
Objective-C has always used reference counting. Every object carries a retain count; when it reaches zero, the object is deallocated. Under Manual Retain Release (MRR, sometimes MRC) you adjust that count by hand.
The model is expressed not in terms of counts but of ownership, which is what makes it tractable: you do not track the absolute count, only the claims you personally hold.
The Four Rules
| Rule | Statement |
|---|---|
1 |
You own any object you create with a method whose name begins with |
2 |
You can take ownership of an object you did not create by sending it |
3 |
You must relinquish ownership of every object you own, exactly once, by sending it |
4 |
You must not relinquish ownership of an object you do not own. |
Everything else follows mechanically from the method’s name:
// You own these -- you must release them.
NSString *a = [[NSString alloc] initWithFormat:@"%d", 42];
NSArray *b = [someArray copy];
NSObject *c = [NSObject new];
[a release];
[b release];
[c release];
// You do NOT own these -- do not release them.
NSString *d = [NSString stringWithFormat:@"%d", 42]; // a "convenience constructor"
NSArray *e = [NSArray arrayWithObject:d];
NSString *f = someObject.name; // just a getter
// Unless you take ownership explicitly:
[d retain];
// … use d for a while …
[d release];
A method not in one of the four families returns an object it does not hand you ownership of — by convention it has already been `autorelease`d, and it will survive until the current autorelease pool drains. This naming convention is not a suggestion: the whole model depends on it, ARC’s optimiser depends on it, and breaking it in your own code causes leaks or crashes in callers.
retain, release and autorelease
NSObject *obj = [[NSObject alloc] init]; // retain count 1 -- you own it
[obj retain]; // 2 -- you now own it twice
[obj release]; // 1
[obj release]; // 0 -- dealloc runs, memory is freed
// obj is now a dangling pointer. Under MRR, defensively:
obj = nil;
autorelease defers the release until the innermost autorelease pool drains. It is what makes it possible to
return an object you created without leaking it and without the caller having to know:
- (NSString *)descriptionText {
NSString *s = [[NSString alloc] initWithFormat:@"%@ (%ld)", self.name, (long)self.age];
return [s autorelease]; // "I'm done owning it, but don't free it yet"
}
Absolute retain counts are not a debugging tool. [obj retainCount] reports an implementation detail — constant strings return NSUIntegerMax, framework objects hold their own references — and reasoning about it
is a reliable route to the wrong conclusion. Reason about your own claims instead.
Autorelease Pools
An autorelease pool holds objects awaiting release and sends release to all of them when it drains. On the
main thread, AppKit and UIKit create a pool at the top of each run-loop iteration and drain it at the end, so
autoreleased objects survive the current event and no longer.
The modern spelling is the @autoreleasepool block, which is faster than the old class and works under both
MRR and ARC:
@autoreleasepool {
NSString *tmp = [NSString stringWithFormat:@"%d", 42]; // autoreleased
} // tmp is released here
// The legacy form, still visible in old code:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// …
[pool drain]; // or [pool release] -- identical on the modern runtime
Two situations require a pool explicitly:
// 1. A tight loop that creates many temporaries: without the inner pool, all one
// million strings accumulate until the loop ends, and memory use explodes.
for (NSInteger i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *s = [NSString stringWithFormat:@"item %ld", (long)i];
[self process:s];
}
}
// 2. A secondary thread: only the main thread gets a pool for free.
- (void)workerThreadBody {
@autoreleasepool {
// … all work here …
}
}
Both still apply under ARC — @autoreleasepool is the one piece of manual memory management that survived.
Accessor Patterns Under MRR
Under MRR you write accessors by hand, and the memory management is yours. The correct retain setter:
- (void)setName:(NSString *)name {
if (_name != name) { // the guard matters -- see below
[_name release];
_name = [name retain];
}
}
The if (_name != name) check is not an optimisation. Without it, obj.name = obj.name releases the object
and then retains a pointer to freed memory. An alternative ordering avoids the guard by retaining first:
- (void)setName:(NSString *)name {
[name retain];
[_name release];
_name = name;
}
The copy variant, for any type with a mutable subclass:
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release];
_name = [name copy]; // copy, not retain -- caller can't mutate it behind us
}
}
And the getter, which must return something that survives long enough to be useful:
- (NSString *)name {
return [[_name retain] autorelease]; // safe even if self is deallocated meanwhile
}
@property (retain) and @synthesize generated exactly these under MRR, which is why the property attributes
retain, copy and assign are named as they are — they name the setter’s memory-management policy.
dealloc Under MRR
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_name release];
[_items release];
_name = nil; // defensive
_items = nil;
[super dealloc]; // LAST, and mandatory -- unlike under ARC
}
Forgetting [super dealloc] leaks the superclass’s storage; calling it first frees the object out from under
the rest of the method. It goes last, always.
Access ivars directly here — not self.name = nil — because a subclass’s setter override should not run on
a partly-destroyed object.
An Object’s Lifecycle Under MRR
count reaches 0 Initialized --> Deallocating: -release,
count reaches 0 Deallocating --> [*]: -dealloc runs
(release ivars, then [super dealloc]) Initialized --> Leaked: owner forgets -release note right of Leaked Memory is never reclaimed. No crash -- just growth. end note Initialized --> Dangling: -release sent
once too often note right of Dangling Object freed while a pointer still refers to it. The next message is undefined behaviour. end note
The Classic Bugs
Leaks
An owned object never released. Nothing crashes; memory simply grows.
- (void)leaky {
NSMutableArray *items = [[NSMutableArray alloc] init]; // owned
[self process:items];
// missing [items release];
}
// Subtler: an early return skips the release.
- (BOOL)alsoLeaky {
NSData *data = [[NSData alloc] initWithContentsOfFile:path];
if (!data) { return NO; }
if (![self validate:data]) { return NO; } // leak on this path
[data release];
return YES;
}
Over-Release
Releasing something you do not own, or releasing twice. This does crash — and typically not at the point of the bug, which is what makes it painful:
NSString *s = [NSString stringWithFormat:@"%d", 42]; // NOT owned
[s release]; // over-release -- crash, eventually
NSString *t = [[NSString alloc] init];
[t release];
[t release]; // double release
The symptom is usually message sent to deallocated instance or a corrupted-heap crash somewhere unrelated.
Dangling Pointers
A pointer that outlived its object. Any message sent through it is undefined behaviour — sometimes a crash, sometimes silently wrong results if the memory has been reused:
NSString *s = [[NSString alloc] init];
[s release];
NSLog(@"%@", s); // undefined: the memory may already hold something else
The historical debugging aid is NSZombie: with NSZombieEnabled set, deallocated objects are replaced by
"zombie" stand-ins that log any message they receive, turning a mystery crash into a precise report.
Instruments' Zombies template does the same graphically, and both still work under ARC — see
Build and Tooling.
Why ARC Fixed This
Each of the above is a mechanical mistake: the compiler can see what the rules require, because the rules
key off method names it already knows. ARC inserts exactly the retain/release/autorelease calls you
would have written, at the right places, on every code path including early returns and exceptions — and it
optimises away pairs that cancel out, making ARC code typically faster than hand-written MRR.
What ARC does not fix is retain cycles, which are a design error rather than a bookkeeping one. That, and the rest of the modern model, is covered in Automatic Reference Counting.
Mixing MRR and ARC
Files are compiled one way or the other, so a mixed project is entirely workable:
# Disable ARC for one legacy file (also settable per-file in Xcode's build phases)
clang -fobjc-arc … -fno-objc-arc LegacyClass.m
An MRR file and an ARC file interoperate freely — the ownership conventions are the same, which is the whole point of the naming rules. Migrate a file at a time; Xcode’s Edit ▸ Convert ▸ To Objective-C ARC automates most of it.
See Also
-
Automatic Reference Counting — the modern model, and what to use.
-
Classes and Objects —
deallocand initialisation. -
Build and Tooling — Instruments, zombies and the static analyzer.