Manual Reference Counting: retain, release, autorelease, and the Ownership Model
Explain the historical Manual Reference Counting (MRC) model that underlies Objective-C memory management — retain, release, and autorelease — state and apply the fundamental ownership rule, and explain precisely why retainCount must never be used to reason about or debug object ownership.
Every modern Objective-C project you will ever touch uses ARC (Automatic Reference Counting), which is covered in the next lesson — but ARC is not a different memory management system from what came before it. ARC is a compiler feature that writes the same retain and release calls a human used to write by hand, inserting them automatically and correctly at compile time. This means the model underneath ARC is still, today, in 2026, fundamentally the reference-counting model described in this lesson: an object's lifetime is governed by a count of how many things currently claim ownership of it, and the object is destroyed the moment that count reaches zero. If you only ever learn the ARC keywords without understanding what they compile down to, ARC's behavior — including its bugs, its edge cases in mixed Objective-C/C code, and the handful of situations where you must still manage memory by hand (bridging to Core Foundation, for instance) — will remain mysterious. This lesson deliberately teaches the 'old', manual way first, exactly as Objective-C programmers practiced it for the better part of two decades, so that ARC in the next lesson reads as 'the compiler doing this for you' rather than as an unrelated, opaque magic trick.
Reference Counting: The Core Idea
Objective-C objects live on the heap and are accessed through pointers, and unlike a local int or struct sitting on the stack, a heap object does not automatically disappear when the function that created it returns. Something has to decide when it is safe to reclaim that object's memory, and Objective-C's answer — inherited from NeXTSTEP's Foundation framework in the 1980s and still true today — is reference counting: every object carries an integer, its retain count, that tracks how many separate parts of the program currently consider themselves an 'owner' of that object. As long as the retain count is greater than zero, the object is guaranteed to stay alive in memory. The instant the retain count reaches zero, the runtime considers the object unreachable by any legitimate owner and deallocates it, calling its dealloc method and reclaiming its memory.
This is a fundamentally different strategy from a tracing garbage collector (used by languages like Java or Go), which periodically pauses and walks the entire graph of live objects from a set of roots, identifying and reclaiming anything unreachable. Reference counting is deterministic and local: there is no background sweep, no unpredictable pause, and an object's destruction is a direct, traceable consequence of its retain count hitting zero at a specific, identifiable moment — which is exactly the moment the last strong owner released it. This determinism is precisely why Objective-C's model, and later ARC, could work well even on memory- and power-constrained mobile hardware, where a stop-the-world garbage collection pause would be unacceptable.
Before ARC existed (Objective-C 2.0's ARC was announced in 2011), every single increment and decrement of that retain count was something the programmer wrote by hand, as an explicit message sent to the object. Get this bookkeeping wrong in either direction and you get one of exactly two categories of bug: release something too many times (or too early) and you get a dangling pointer, use-after-free, and typically a crash (EXC_BAD_ACCESS); fail to release something enough times and you get a memory leak — the object lives forever, silently consuming memory, because nothing ever brought its count down to zero. Manual Reference Counting is the discipline of getting every increment paired with exactly the right decrement.
retain: Claiming Ownership
Sending an object the retain message increments its retain count by one and returns the object itself (specifically self, i.e. the very same pointer you sent retain to), which is why retain calls are commonly chained directly into assignment: self.thing = [otherThing retain];. Calling retain is how you declare, explicitly and in code, 'I am now also an owner of this object; do not deallocate it out from under me, even if everyone else releases their claim.' Every object created via alloc, new, or copy already starts with a retain count of 1 — the creator is implicitly its first owner — so retain is specifically for the case where you did not create the object yourself but still need to guarantee it survives for as long as you hold a reference to it (storing it in an instance variable being the canonical example).
A very common MRC pattern was writing a custom setter by hand that correctly retained the incoming value and released the old one, precisely the logic that a strong property attribute (covered in the Properties lesson) and, later, ARC, would come to generate automatically: - (void)setThing:(Thing *)newThing { [newThing retain]; [thing release]; thing = newThing; }. Notice the order — retain the new value before releasing the old one. If newThing happened to be the very same object as the current thing (self-assignment), releasing first could drop the retain count to zero and deallocate the object before you ever got a chance to retain it again, leaving thing pointing at freed memory. This retain-before-release ordering is one of the most important idioms in all of MRC-era Objective-C, and understanding why it matters is a direct rehearsal for understanding exactly what ARC's synthesized strong setter does for you silently today.
release: Relinquishing Ownership
Sending release decrements an object's retain count by one, and — critically — if that decrement brings the count to zero, the object is deallocated immediately, synchronously, right then, as part of that very release call. There is no delay and no background cleanup pass: the moment the last owner releases its claim, dealloc runs and the memory is reclaimed. This is why, in MRC code, you must never send any further messages to an object pointer after releasing it unless you are certain some other owner still holds a retaining reference — the pointer may now be dangling, referring to memory that has already been freed and could be reused for something else entirely, producing corrupted or crash-inducing behavior that is notoriously hard to reproduce.
The foundational rule governing when you must call release (or autorelease, discussed next) is usually stated as: if you own an object, you must relinquish ownership of it when you are done with it, and you own an object if and only if you created it using a method whose name begins with alloc, new, copy, or mutableCopy, or if you sent it retain yourself. This is often called the 'NARC' rule as a mnemonic (New, Alloc, Retain, Copy) — those four verbs, and only those, hand you ownership, and ownership always comes with a corresponding obligation to give it back via release or autorelease once you are finished. Conversely, if you merely received an object from some other method — say, an object returned by a Foundation convenience constructor like [NSString stringWithFormat:...] — you do not own it and must not release it; it is not yours to release, and doing so anyway breaks the ownership contract and will eventually over-release someone else's object.
• You own an object (and must eventually release it) if you obtained it via alloc, new, copy, mutableCopy, or retain. • You do not own an object you merely received as a return value from an ordinary convenience method — do not release it. • release decrements the retain count synchronously; hitting zero triggers dealloc immediately, in that same call.
autorelease: Deferred Release
Convenience constructors like [NSString stringWithFormat:@"%d", 42] present a puzzle: the method has to create the object with alloc/init internally (so it starts life owned, with a retain count of 1), but the caller of stringWithFormat: is not supposed to own the result and must not release it. If the method simply returned the object without doing anything else, it would leak, because nothing would ever balance out that initial retain count of 1. The solution Objective-C uses is autorelease: instead of releasing the object immediately before returning it (which would deallocate it before the caller ever got to use it), the method sends it autorelease, which registers the object with the current autorelease pool and returns the object unchanged, retain count still intact, still fully alive and usable.
An autorelease pool is, conceptually, just a list of objects waiting to be sent release later. At some well-defined point — historically, at the end of each iteration of the application's run loop, or explicitly wherever an @autoreleasepool { ... } block ends — the pool is 'drained', meaning every object registered in it receives exactly one release message, in a batch, all at once. This is precisely how convenience constructors give you an object you can use immediately and safely, without you owning it or being responsible for releasing it: the pool becomes the object's temporary owner, and later drains that ownership on your behalf, deterministically, just usually not at a moment you directly control.
This deferred-release mechanism is the single biggest reason retainCount cannot be trusted, and it is the direct segue into why the next section forbids using it for debugging: an object might sit in an autorelease pool holding an extra, invisible pending retain-count decrement that simply hasn't fired yet, meaning the number you observe right now does not represent the object's 'final' or 'true' ownership state at all — it represents a snapshot mid-way through a bookkeeping process you cannot fully see or predict from application code.
retainCount: Why It Exists and Why You Must Never Debug With It
Every NSObject responds to a retainCount method that returns the object's current retain count as an integer, and it is tempting — especially for a beginner trying to hunt down a crash or a leak — to sprinkle NSLog(@"%lu", (unsigned long)[obj retainCount]); through code to 'watch' an object's ownership. Apple's own documentation explicitly and strongly warned against this practice for as long as retainCount existed, and the method is formally deprecated today. The reason is not merely stylistic; the number retainCount returns is, in almost every practical situation, not something you can correctly interpret, for several independent reasons.
First, the Objective-C runtime and Foundation framework hold plenty of retains you never asked for and cannot see from your code: small integer NSNumber instances and certain singleton or tagged-pointer objects may report artificially large or nonsensical retain counts because of internal runtime optimizations (tagged pointers do not use conventional reference counting representation at all, so asking for their retainCount returns a meaningless value). Second, as the autorelease discussion above showed, an object can have pending release messages sitting in an autorelease pool that have not fired yet — the count you observe right now is not the count that will exist a moment from now, after the pool next drains, and you generally cannot know when that will happen. Third, and most fundamentally, retainCount only ever tells you the aggregate total across every owner in the entire process — it can never tell you which specific piece of code holds which specific retain, so even a perfectly accurate number gives you zero information about where a suspected extra retain (or missing release) actually lives in your codebase. A retain count of 3 doesn't tell you if that's correct or a bug; you have no way to know what the 'right' number should be without independently reasoning through every owner by hand, which is the actual debugging work retainCount tempts you to skip.
The correct way to debug ownership problems, in MRC-era code and in mixed ARC/legacy code alike, is to reason about ownership statically, by reading the code: trace every alloc/new/copy/retain to a matching release/autorelease, the same way you'd balance parentheses. Tools built for exactly this job — the Clang Static Analyzer, Instruments' Leaks and Zombies templates, and the NSZombie / Malloc Guard debugging features — are designed to catch over-release, use-after-free, and leak bugs by instrumenting actual allocation and deallocation events, not by asking an unreliable object for a number that was never meant to be introspected by application code in the first place.
The Ownership Rule, Stated Precisely
Pulling the previous sections together into one rule: if you create an object using alloc, new, copy, or mutableCopy, or if you explicitly retain an object someone else created, you own it, and you are obligated to send it exactly one matching release (or autorelease) when you are finished with it — no more, no fewer. If you merely receive an object as a parameter or as an ordinary method's return value, you do not own it, may not assume it will remain alive beyond the current scope unless you retain it yourself, and must not release it.
This single rule, consistently applied across an entire codebase by every single class and every single method, is what made large Objective-C programs possible without a garbage collector: as long as every method obeyed the convention, ownership could be reasoned about locally, one method at a time, without needing global knowledge of the whole object graph. Violating it in either direction breaks the whole system for everyone who touches that code afterward — an over-release introduces a crash that might not manifest until much later, when the freed memory happens to get reused, and an under-release introduces a slow leak that might not be noticed until the app has been running for hours.
Why This History Still Matters Under ARC
It would be reasonable to ask why a course teaching modern Objective-C spends an entire lesson on a system nobody writes by hand anymore. The answer is that ARC does not replace this model — it automates it. Every strong property you will write under ARC compiles down to exactly the retain-before-release setter pattern shown earlier in this lesson; every local strong-typed variable going out of scope compiles down to an inserted release call at the end of its scope; and the entire concept of an autorelease pool is still alive and well under ARC, still governing when convenience-constructed objects actually get their pending release. None of that vocabulary goes away — ARC just writes it for you, correctly, every time, instead of leaving it to human discipline that inevitably slips under deadline pressure.
There are also situations, covered in the next lesson's discussion of bridging, where ARC's automatic management stops at a boundary — crossing into Core Foundation APIs, or into old non-ARC code compiled with -fno-objc-arc — and you are, once again, briefly responsible for balancing retains and releases by hand, using exactly the ownership rule from this lesson. A developer who never learned MRC has no mental model for what a CFRetain/CFRelease pair, or a __bridge_retained cast, is actually doing, and is far more likely to get it wrong. Learning the manual model first is what makes every later, more convenient layer legible rather than magical.
Code Examples
alloc/init hands you an object you own outright, with an implicit retain count of 1. Calling retain a second time adds a second claim of ownership, so it now takes two matching release calls to bring the count back to zero. The second release is the one that actually triggers deallocation — this happens immediately, in that exact call, not at some later point.
[person release]; // the second one brings the count to 0 and deallocates
This is the canonical MRC setter idiom for an owning (strong) instance variable. Retaining the new value before releasing the old one protects against the self-assignment case (setEngine: called with the object already stored in engine) — releasing first could deallocate the object before the retain ever happens, leaving engine pointing at freed memory.
Common mistake: Writing [engine release]; [newEngine retain]; engine = newEngine; — releasing before retaining is unsafe under self-assignment and is one of the most cited MRC bugs in Apple's own historical documentation.
alloc/initWithFormat: gives the method itself ownership (retain count 1). Because the method's name does not begin with alloc/new/copy/mutableCopy, the caller must NOT own the returned value — so instead of releasing it outright (which would deallocate it before the caller ever touches it), the method registers it with autorelease, deferring the matching release until the pool next drains. This is exactly why Foundation's countless convenience constructors are safe to chain directly into expressions without any release bookkeeping on the caller's part. If the method instead returned result directly without autorelease, the string would leak every single time this method was called, because nothing would ever balance the initial retain count of 1.
The retainCount call returns a technically accurate snapshot, but that snapshot is useless for reasoning about ownership: it does not reveal the pending, invisible release already queued in the autorelease pool, nor does it tell you which specific code path holds the remaining claim. This is the concrete mechanism behind the rule stated earlier in the lesson: retainCount must never be used to debug ownership.
Person *person = [[Person alloc] init]; // retain count starts at 1; you own it
[person retain]; // retain count is now 2; you claimed an extra ownership
NSLog(@"%@", person.name);
[person release]; // retain count back to 1
[person release]; // retain count reaches 0 -> dealloc runs now, synchronously- (void)setEngine:(Engine *)newEngine {
if (newEngine != engine) {
[newEngine retain]; // claim ownership of the new value FIRST
[engine release]; // then release the old value
engine = newEngine; // finally, update the ivar to point at the new object
}
}+ (NSString *)greetingForName:(NSString *)name {
NSString *result = [[NSString alloc] initWithFormat:@"Hello, %@!", name];
return [result autorelease]; // hand off ownership to the current autorelease pool
}
// Calling code does NOT own the returned string and must not release it:
NSString *greeting = [Person greetingForName:@"Ada"];
NSLog(@"%@", greeting); // safe to use immediately@autoreleasepool {
NSObject *obj = [[[NSObject alloc] init] autorelease];
NSLog(@"retain count right now: %lu", (unsigned long)[obj retainCount]);
// Prints 1 -- but this '1' is already scheduled to be decremented
// to 0 the moment this @autoreleasepool block ends. The number tells
// you nothing about the pending release sitting in the pool.
}
// obj has now been deallocated here, as the pool drained.Why every senior Objective-C engineer still knows this cold
Large, long-lived Objective-C codebases — many iOS and macOS apps still shipping today were started well before ARC existed in 2011 — often contain files or entire modules still compiled under MRC, deliberately, because migrating them to ARC risked subtle behavior changes the team wasn't ready to audit. Any engineer who touches such a file, or who writes code that bridges to Core Foundation (CoreGraphics, CoreText, and much of the low-level media stack still expose C-style CF types with manual CFRetain/CFRelease semantics), is doing exactly the bookkeeping described in this lesson, by hand, today, regardless of what year it is. Interview questions about Objective-C memory management routinely ask candidates to walk through exactly this ownership rule from first principles, precisely because it demonstrates whether someone understands what ARC is doing for them or has only ever memorized ARC's keywords without the underlying model.
Sprinkling NSLog calls that print [obj retainCount] throughout a method to try to 'watch' when an object gets freed.
Trace alloc/new/copy/retain calls to their matching release/autorelease by reading the code, or use Instruments (Leaks/Zombies) and the Clang Static Analyzer.
retainCount can be inflated by runtime internals (tagged pointers, singletons), deflated by pending autorelease pool drains that haven't fired yet, and never tells you which code path owns which retain — it is not a debugging signal, and Apple deprecated the method specifically because developers misused it this way.
[engine release]; [newEngine retain]; engine = newEngine;
[newEngine retain]; [engine release]; engine = newEngine;
Releasing the old value before retaining the new one is unsafe when the new value and the old value are the same object (self-assignment): the release could drop the retain count to zero and deallocate the object before you get a chance to retain it again, leaving a dangling pointer.
Releasing an object you received as a plain method parameter or as the return value of an ordinary (non-alloc/new/copy) method, because 'it seemed safer.'
Only release objects you actually own per the ownership rule — obtained via alloc/new/copy/mutableCopy, or explicitly retained.
Releasing an object you don't own decrements a retain count that some other part of the program is depending on, which can deallocate the object while its rightful owner still believes it is alive — a classic over-release bug that produces a crash somewhere seemingly unrelated, later.
Returning a freshly alloc/init'd object directly from a convenience constructor without autorelease or a matching release.
Send the freshly created object autorelease before returning it from a convenience constructor, exactly as Foundation's own methods like stringWithFormat: do internally.
Without autorelease (or a release the caller is somehow expected to perform, which breaks the convenience-constructor convention entirely), the object's initial retain count of 1 is never balanced, and every call to the method leaks one object.
Under Manual Reference Counting, which four method-name prefixes hand you ownership of the returned object (the 'NARC' rule)? A. get, fetch, load, and find B. alloc, new, copy, and mutableCopy (or explicitly sending retain) C. init, create, build, and make D. Any method that returns an object pointer
B. alloc, new, copy, and mutableCopy (or explicitly sending retain) — The ownership rule specifically ties ownership to alloc, new, copy, mutableCopy, or an explicit retain message. Any other method's return value is not owned by the caller by default.
What does sending an object the autorelease message actually do? A. Immediately deallocates the object B. Immediately decrements the retain count, but keeps the object's memory reserved for later reuse C. Registers the object with the current autorelease pool so it receives a release message later, when the pool drains, without releasing it right now D. Increments the retain count so the object survives longer than normal
C. Registers the object with the current autorelease pool so it receives a release message later, when the pool drains, without releasing it right now — autorelease defers the matching release to the moment the current autorelease pool drains, leaving the object fully alive and usable right now while still eventually balancing its retain count.
Why is retainCount unreliable for debugging ownership bugs? A. It always returns 0 for objects created with alloc B. It is a private method that cannot actually be called from application code C. It reflects only the current aggregate count, which can be affected by runtime internals and pending autorelease pool drains, and it never reveals which specific code path holds which retain D. It only works correctly on NSString objects
C. It reflects only the current aggregate count, which can be affected by runtime internals and pending autorelease pool drains, and it never reveals which specific code path holds which retain — retainCount can be skewed by runtime optimizations like tagged pointers, doesn't account for retains sitting in an as-yet-undrained autorelease pool, and even when accurate gives no information about which code owns which claim — which is the actual information needed to debug an ownership bug.
In a hand-written MRC setter, why must the new value be retained before the old value is released? A. It has no effect either way; the order is purely stylistic B. Retaining first is required by the Objective-C compiler's grammar C. It protects against self-assignment: releasing the old value first could deallocate the object before it is retained again, if the new and old values are the same object D. Releasing first would cause a compiler warning
C. It protects against self-assignment: releasing the old value first could deallocate the object before it is retained again, if the new and old values are the same object — If newValue and the current ivar refer to the same object, releasing first can bring the retain count to zero and deallocate it before the retain call ever executes, leaving the ivar pointing at freed memory.
An object is created with alloc/init and never sent release, retain, or autorelease again anywhere in the program. What happens? A. The object is automatically deallocated when the app terminates, with no consequences B. The object leaks: its retain count never reaches zero, so dealloc is never called and its memory is never reclaimed while the app runs C. The runtime deallocates it automatically after a short timeout D. The compiler refuses to build the program
B. The object leaks: its retain count never reaches zero, so dealloc is never called and its memory is never reclaimed while the app runs — alloc/init leaves the object with a retain count of 1, owned by the code that created it. If that ownership is never relinquished via release or autorelease, the count never reaches zero and the object leaks for the life of the process.
How does reference counting (as used by Objective-C) fundamentally differ from a tracing garbage collector? A. Reference counting requires more total memory in every case B. Reference counting deallocates an object deterministically the instant its count reaches zero, rather than periodically scanning the whole object graph from a set of roots C. Tracing garbage collectors cannot ever free memory that reference counting can D. There is no meaningful difference; they are two names for the same technique
B. Reference counting deallocates an object deterministically the instant its count reaches zero, rather than periodically scanning the whole object graph from a set of roots — Reference counting ties deallocation to a specific, local, deterministic event (the last release bringing the count to zero), whereas a tracing collector periodically walks the entire live object graph from roots and can introduce unpredictable pause times.
Write the body of a hand-written MRC setter method, -setAccount:, for an ivar named _account of type Account *, following the correct retain-before-release ordering, and explain in one sentence why the ordering matters for the case [obj setAccount:obj.account].
• (void)setAccount:(Account *)newAccount { [newAccount retain]; [_account release]; _account = newAccount; } // Ordering matters because if newAccount and the current _account are the same object, // releasing before retaining could drop the retain count to zero and deallocate the // object before the method ever gets a chance to re-retain it, leaving _account dangling.
Retain the incoming value first, then release the old value, then assign — in that exact order — exactly as shown in the setEngine: example in this lesson.