ARC: Automatic Reference Counting, Ownership Qualifiers in Practice, and Bridging

Explain what Automatic Reference Counting actually is (a compile-time transformation, not a garbage collector), describe the ownership rules ARC enforces, correctly apply strong, weak, unsafe_unretained, and autoreleasing, and correctly use bridging casts to cross ARC/non-ARC and Core Foundation boundaries.

The previous lesson walked through Manual Reference Counting in full: retain, release, autorelease, and the ownership rule that every Objective-C programmer once had to apply by hand, on every single method, forever. ARC, introduced with the Objective-C 2.0 compiler in 2011 and standard in essentially all Objective-C code written since, does not change that underlying model at all — it automates it. The compiler now performs the exact same static analysis a careful MRC programmer would have done by hand, and inserts the exact same retain, release, and autorelease calls at exactly the points they were always needed, correctly and consistently, every single time, without you writing a single one yourself. Understanding ARC well means understanding it as a compile-time code transformation layered directly on top of the model from the previous lesson — not as an unrelated, higher-level feature, and definitely not as garbage collection, a genuinely different technique that Objective-C briefly experimented with (in NSGarbageCollector, later removed entirely) and abandoned in favor of ARC.

ARC Is a Compiler Feature, Not a Runtime Garbage Collector

The single most important fact about ARC, and the one most often misunderstood by programmers coming from garbage-collected languages, is that ARC does its work entirely at compile time. When clang compiles an ARC-enabled .m file, it performs static analysis of every strong-typed variable's scope and every property assignment, and inserts calls to retain, release, and autorelease directly into the generated machine code — the exact same underlying runtime functions used by MRC. There is no background thread scanning memory while your app runs, no periodic pause to walk an object graph, and no runtime component deciding, dynamically, what is and isn't reachable. By the time your app is actually executing, ARC-inserted retain/release calls are indistinguishable, at the machine-code level, from calls an MRC programmer would have typed by hand — the only difference is that a human never typed them, and never can forget one, get the order wrong, or over-release.

This has real, practical consequences. Because ARC's decisions are made statically, at compile time, based on the compiler's ability to see how long a reference needs to stay valid within its scope, ARC's behavior is fully deterministic and predictable in exactly the way MRC's manual bookkeeping was — an object deallocates at a precise, identifiable point, the moment its last strong reference goes out of scope or is reassigned, never at an unpredictable moment chosen by a collector. This is also why ARC integrates cleanly with autorelease pools exactly as described in the previous lesson: ARC still uses autorelease under the hood in many situations (returning an object from a method, for instance), and @autoreleasepool { } blocks still exist and still matter under ARC, particularly inside tight loops that create many temporary objects, to bound peak memory use between drains.

The Ownership Rules ARC Enforces

ARC enforces the same ownership rule from the previous lesson, but now as a set of compiler-checked constraints on your source code rather than a convention you had to remember. Under ARC, you are simply not permitted to call retain, release, autorelease, or dealloc directly — the compiler rejects the code outright, because it has taken over responsibility for calling them itself and cannot allow your manual calls to conflict with its own inserted ones. Every object pointer variable is annotated, explicitly or (far more commonly) by inferred default, with an ownership qualifier that tells the compiler exactly what obligation that variable represents: does holding this pointer keep the object alive, or not?

Because the compiler now owns this bookkeeping completely, an entire category of MRC bug simply cannot occur in pure ARC code: you cannot over-release (there is no release call for you to send twice), and you cannot forget a release that leaks an object your own code created and held onto too long unowned — the compiler inserts every required release at the correct end-of-scope point, every time, without exception. What remains possible, and remains exactly as real a bug category as it was under MRC, is a retain cycle: two or more objects holding strong references to each other in a loop, each keeping the other's retain count above zero forever, so that neither ever reaches zero and neither is ever deallocated. ARC does not detect or break retain cycles automatically — this is precisely why weak and unsafe_unretained exist, and precisely why understanding them is not optional.

strong: The Default, Ownership-Keeping Qualifier

strong is the default ownership qualifier for any Objective-C object pointer under ARC — if you declare NSString *name; as a local variable, an instance variable, or a property attribute list with no explicit qualifier, it is strong by default (properties are the one place this default has a documented historical wrinkle: pre-ARC and in careless code you might see assign, but under ARC's own defaulting rules, an object-typed property with no explicit ownership attribute is strong). A strong reference is a full claim of ownership in exactly the MRC sense from the previous lesson: as long as any strong reference to an object exists anywhere, the object's retain count stays above zero and it is guaranteed to remain alive. Assigning a new value to a strong variable causes ARC to insert a retain on the new value and a release on whatever the variable previously held, in precisely the retain-before-release order the hand-written MRC setter example demonstrated — you get that correct ordering for free, every time, because the compiler is generating it, not typing it.

The vast majority of object references in ordinary Objective-C code should be strong, and this is exactly as it should be: a parent object holding onto its child data (an array holding onto the objects it contains, a view controller holding onto its model), a local variable holding a temporary result you need to keep using within the current scope — these are all textbook strong-reference relationships, and strong is the right default for almost everything you write.

weak: A Non-Owning Reference That Auto-Nils

A weak reference does not keep the referenced object alive — if every strong reference to an object is gone, a weak reference pointing at it does nothing to prevent deallocation. What makes weak distinctly different from a plain, unmanaged C pointer is that the Objective-C runtime actively tracks every weak reference to every object, and the instant that object is deallocated, the runtime automatically sets every weak pointer that referred to it to nil — a mechanism often called 'zeroing weak references.' This is exactly the behavior captured in the diagram accompanying this lesson: a strong reference keeps an object alive for as long as it exists, while a weak reference does not keep the object alive and is automatically zeroed out the moment the object it pointed to is deallocated.

The single most important, everyday use of weak is breaking retain cycles in parent/child and delegate relationships. Consider a delegate pattern: a UITableView needs a reference back to its delegate to ask it questions (numberOfRowsInSection:, etc.), and the delegate — very often the view controller that owns the table view in the first place — needs a reference to the table view to configure it. If both of those references were strong, you would have a textbook retain cycle: the table view keeps the delegate alive, the delegate keeps the table view alive, and neither is ever deallocated even after the view controller that should own both of them is dismissed. Declaring the delegate property weak breaks this cycle cleanly: the table view can still message its delegate whenever it needs to, but it does not keep the delegate artificially alive, and when the real owner (the view controller) is deallocated, the delegate reference the table view holds automatically becomes nil rather than dangling.

Sending a message to nil in Objective-C is explicitly well-defined and safe — it simply does nothing and returns a zeroed value of the appropriate return type — which is exactly why the zeroing behavior of weak references matters so much for safety: a weak reference that has auto-nilled is inert and harmless to message, whereas the alternative (a pointer left dangling, still pointing at freed memory) would be a serious, hard-to-diagnose crash bug.

unsafe_unretained: A Non-Owning Reference That Does Not Auto-Nil

unsafe_unretained shares half of weak's behavior and is missing the other, critical half: like weak, it does not keep the referenced object alive and does not contribute to its retain count. Unlike weak, it is not tracked by the runtime at all, and when the object it points to is deallocated, an unsafe_unretained variable is left holding a dangling pointer — it is not automatically set to nil, and the 'unsafe' in its name is exactly this: it is unsafe to use once the object it pointed to has gone away, because the runtime has no mechanism to warn you or clean it up.

This qualifier exists mainly for two situations. First, historical and compatibility reasons: the zeroing-weak-reference machinery that makes weak safe requires runtime support that was not available on every platform ARC originally targeted (early iOS versions running the 'legacy' runtime lacked it, as did 32-bit OS X targets for a period), so unsafe_unretained was the only non-owning option available there, and you may still encounter it in code written for, or still supporting, those older targets. Second, certain low-level or performance-sensitive contexts and some pre-ARC-aware APIs are still declared with unsafe_unretained for compatibility even on modern platforms — for instance, NSMapTable and similar collection-adjacent APIs from before weak references matured, or certain block signatures. In ordinary modern application code, weak is almost always the correct choice, and unsafe_unretained should be reserved for the specific, rare situations where you have a concrete reason the zeroing behavior is unavailable or unwanted — using it as a casual substitute for weak, purely out of habit, reintroduces exactly the dangling-pointer risk ARC exists to eliminate.

autoreleasing: Out-Parameters and NSError **

The fourth ownership qualifier, __autoreleasing, applies specifically to a pointer-to-a-pointer used as an out-parameter — the pattern you see constantly in Foundation and Cocoa APIs that report errors via an NSError ** parameter, such as - (BOOL)writeToFile:(NSString *)path error:(NSError **)error;. Inside such a method, the implementation typically does something like *error = [NSError errorWithDomain:...];, handing an object back to the caller through that out-parameter. Because the method itself doesn't retain a long-term reference to that error object, and the caller needs it to survive at least long enough to actually read it after the call returns, ARC implicitly qualifies the pointee of an id * (or specifically-typed NSError **) parameter as __autoreleasing unless told otherwise — meaning the object handed back through it is autoreleased, guaranteeing it survives at least until the next autorelease pool drain, which is comfortably long enough for the calling code to inspect it.

The everyday, calling-code pattern looks like NSError *error = nil; BOOL ok = [data writeToFile:path options:0 error:&error]; if (!ok) { NSLog(@"%@", error); }. You almost never write __autoreleasing explicitly yourself — it is the implicit qualifier ARC applies to the pointee of &error in exactly this situation, and knowing the name and mechanism behind it demystifies what would otherwise look like special-cased magic syntax around the extremely common NSError ** pattern.

Bridging: Crossing the ARC / Core Foundation Boundary

ARC manages Objective-C object pointers, but plenty of Apple frameworks — CoreGraphics, CoreText, CoreFoundation itself — still expose C-style opaque pointer types (CGImageRef, CTFontRef, CFStringRef, and so on) governed by the same manual CFRetain/CFRelease reference-counting discipline the previous lesson covered for Objective-C objects, but ARC does not manage them, because they are not Objective-C objects. Many Core Foundation types are 'toll-free bridged' with a corresponding Foundation class — CFStringRef and NSString * are the classic example — meaning the exact same underlying memory layout can be interpreted as either type, and casting between them is possible, but ARC needs to be told, explicitly, what that cast means for ownership, because ARC cannot see or manage a plain CFStringRef's retain count on its own.

Three bridging casts exist for exactly this purpose. __bridge performs a pure type cast with no transfer of ownership at all — you are telling the compiler 'treat this pointer as the other type, but ARC's memory management responsibility for it is unchanged'; if you bridge a CFStringRef to an NSString * with plain __bridge, ARC does not take ownership, and you remain responsible for eventually calling CFRelease on the original CFStringRef yourself. __bridge_transfer (equivalently CFBridgingRelease) casts a Core Foundation object to an Objective-C object and simultaneously transfers ownership to ARC — ARC now considers itself responsible for releasing it when the resulting strong reference's scope ends, and you must not also call CFRelease yourself, or you will over-release. __bridge_retained (equivalently CFBridgingRetain) does the reverse: it casts an ARC-managed Objective-C object to a Core Foundation type and simultaneously balances a retain on ARC's behalf, handing you a reference you are now manually responsible for releasing with CFRelease when you are done with it, exactly as if you had called CFRetain yourself under MRC rules.

Getting bridging wrong is one of the last remaining places a modern, otherwise pure-ARC codebase can still leak memory or crash from an over-release, because it is precisely the seam where ARC's automatic bookkeeping stops and manual, MRC-style bookkeeping picks back up. The rule to hold onto is simple even though the three spellings look similar: __bridge changes nothing about who is responsible for release; __bridge_transfer hands responsibility to ARC (so you stop being responsible); __bridge_retained takes responsibility away from ARC and hands it to you (so you must call CFRelease yourself, later).

Reading ARC Code With the MRC Model in Mind

A genuinely useful way to read any piece of ARC code, especially while first learning it, is to mentally substitute in the retain/release calls ARC would insert, using exactly the vocabulary from the previous lesson: a strong property's synthesized setter is the retain-before-release pattern; a local strong variable going out of scope at the end of a method is a release call inserted right there, at that closing brace; a method returning a freshly alloc/init'd object is, under the hood, still autoreleasing it (or, in many cases, using a further ARC optimization called 'objc_autoreleaseReturnValue' paired with a matching 'objc_retainAutoreleasedReturnValue' at the call site to avoid the overhead of an actual pool registration, when the compiler can prove it's safe to do so) — the point isn't to memorize that optimization's name, but to internalize that these mechanisms are not new concepts, they are the previous lesson's mechanisms, generated for you.

This mental habit is what makes retain cycles, the main memory bug ARC does not eliminate, intuitive to spot: if you can picture object A's ivar holding a strong reference to B, and B's ivar holding a strong reference back to A, you can picture — in MRC terms — that A's retain on B and B's retain on A never get released by anything, because nothing outside the cycle ever drops the last strong reference into it, exactly a leak in the MRC sense from the previous lesson, just produced by a cycle rather than a forgotten release call.

Code Examples

ViewController strongly owns its Reporter, and Reporter's delegate property points back at the ViewController but is declared weak, so it does not add to the ViewController's retain count. If delegate were also strong, ViewController would keep Reporter alive while Reporter kept ViewController alive, and neither could ever be deallocated even after the ViewController should have gone away.

@property (nonatomic, weak) id<ReporterDelegate> delegate;

Because unsafe_unretained is not tracked by the runtime, deallocating the object it pointed to leaves the pointer pointing at freed memory instead of automatically becoming nil the way a weak reference would. This example is deliberately contrasted with the weak delegate example above: swapping unsafe_unretained for weak here would make cache.lastAccessedItem safely become nil instead of dangling.

Common mistake: Using unsafe_unretained purely out of habit or because it looks similar to weak, without a specific reason the zeroing behavior of weak is unavailable or unwanted.

The address-of expression &error passed for the NSError ** parameter is implicitly treated by ARC as pointing at an __autoreleasing location: inside dataWithContentsOfFile:options:error:, assigning *error = someNSError hands the caller an object guaranteed to survive at least until the next autorelease pool drain, comfortably long enough for the if-block above to read it. You virtually never write __autoreleasing explicitly; it is the implicit qualifier ARC infers for the pointee of an NSError ** (or id *) out-parameter.

Each cast answers a different question about who is responsible for the eventual release. __bridge changes only the pointer's type, leaving responsibility exactly where it was (with you, for the CF side). __bridge_transfer hands a Core Foundation object's existing ownership over to ARC, after which you must not release it yourself. __bridge_retained takes an ARC-managed object and hands you a manually-owned Core Foundation reference, balanced with a retain on ARC's behalf, that you alone are now responsible for releasing with CFRelease.

Common mistake: Calling CFRelease after a __bridge_transfer cast (double-releasing, since ARC already took responsibility), or forgetting to call CFRelease after a __bridge_retained cast (leaking, since ARC no longer will).

@interface Reporter : NSObject
@property (nonatomic, weak) id<ReporterDelegate> delegate;   // does not keep the delegate alive
@end

@interface ViewController : UIViewController <ReporterDelegate>
@property (nonatomic, strong) Reporter *reporter;             // owns the reporter
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.reporter = [[Reporter alloc] init];
    self.reporter.delegate = self;   // weak: does not create a cycle
}
@end
@interface Cache : NSObject
@property (nonatomic, unsafe_unretained) NSObject *lastAccessedItem;
@end

NSObject *item = [[NSObject alloc] init];
Cache *cache = [[Cache alloc] init];
cache.lastAccessedItem = item;
item = nil;   // the only strong reference is gone; the object is deallocated now
// cache.lastAccessedItem is now a DANGLING pointer -- it was never zeroed.
// Sending it any message here is undefined behavior and can crash unpredictably.
NSError *error = nil;   // caller owns this local strong variable
NSData *data = [NSData dataWithContentsOfFile:@"/tmp/missing.txt"
                                        options:0
                                          error:&error];
if (data == nil) {
    NSLog(@"Failed to read file: %@", error.localizedDescription);
}
// __bridge: no ownership transfer -- you still owe CFRelease yourself.
CFStringRef cfName = CFStringCreateWithCString(NULL, "Ada", kCFStringEncodingUTF8);
NSString *name1 = (__bridge NSString *)cfName;
NSLog(@"%@", name1);
CFRelease(cfName);   // still required -- __bridge did not transfer ownership

// __bridge_transfer: ARC now owns the result; do NOT call CFRelease yourself.
CFStringRef cfName2 = CFStringCreateWithCString(NULL, "Grace", kCFStringEncodingUTF8);
NSString *name2 = (__bridge_transfer NSString *)cfName2;
// no CFRelease here -- ARC will release name2 when its strong scope ends

// __bridge_retained: hands a +1 CF reference to you; YOU must CFRelease it later.
NSString *name3 = @"Margaret";
CFStringRef cfName3 = (__bridge_retained CFStringRef)name3;
// ... use cfName3 via Core Foundation APIs ...
CFRelease(cfName3);   // required -- __bridge_retained made you responsible

Strong vs. Weak References Under ARC

A strong reference is a full claim of ownership: as long as one exists, the object cannot be deallocated. A weak reference makes no such claim and is automatically zeroed to nil by the runtime the instant the object it referred to is deallocated, which is what makes it safe to hold without keeping the object alive.

Strong Reference
      ↓
Object remains alive

Weak Reference
      ↓
Does not keep object alive
      ↓
Becomes nil when object deallocates

Where ARC still hands memory management back to you

Even in a codebase written entirely after 2011 and fully adopting ARC from day one, engineers routinely hit two situations from this lesson: fixing a retain cycle between a view controller and a closure-capturing block or a delegate that was accidentally declared strong, usually discovered via Instruments' Leaks or Xcode's Memory Graph Debugger; and getting a bridging cast wrong when calling into CoreGraphics or CoreText APIs to do custom drawing or text layout, where CGImageRef, CGColorRef, and CTFontRef values still flow across the exact __bridge / __bridge_transfer / __bridge_retained boundary described here. Both are considered core, non-optional Objective-C competencies precisely because ARC automates the common case but deliberately leaves these two categories for the engineer to reason about.

Making a delegate property strong because 'strong is the safe default.'

Declare delegate properties (and any back-reference in a parent/child relationship) weak.

A strong delegate reference, combined with the owner typically holding a strong reference back to the thing it delegates to, creates a retain cycle that ARC cannot detect or break on its own, leaking both objects for the life of the app.

Reaching for unsafe_unretained as a casual substitute for weak because the names sound similar.

Default to weak for any non-owning reference; reserve unsafe_unretained for specific legacy-runtime or API-compatibility situations.

unsafe_unretained does not auto-nil when the referenced object is deallocated, so it can silently become a dangling pointer, reintroducing exactly the use-after-free risk ARC exists to eliminate.

Calling CFRelease on a Core Foundation object after casting it to an Objective-C type with __bridge_transfer.

After __bridge_transfer, ARC owns the object; do not call CFRelease yourself.

__bridge_transfer already transfers the existing +1 ownership to ARC. Calling CFRelease afterward decrements a retain count ARC is also going to decrement when its strong reference's scope ends, producing an over-release.

Casting an ARC object to a Core Foundation type with __bridge_retained and then never calling CFRelease on the result.

Every __bridge_retained cast hands you a +1 reference you must balance with your own CFRelease call once you're done with it.

__bridge_retained performs a retain on ARC's behalf specifically so that you can hold the Core Foundation reference safely outside of ARC's management; failing to release it later is a straightforward leak, identical in kind to forgetting a release under MRC.

What does ARC actually do, mechanically, to manage memory? A. It runs a background thread that periodically scans the heap for unreachable objects B. It inserts retain, release, and autorelease calls into your code at compile time, based on static analysis of variable scope and ownership C. It replaces reference counting entirely with a mark-and-sweep garbage collector D. It prevents objects from ever being deallocated until the app terminates

B. It inserts retain, release, and autorelease calls into your code at compile time, based on static analysis of variable scope and ownership — ARC is a compile-time transformation: the compiler statically analyzes your code's scopes and ownership qualifiers and inserts the same retain/release/autorelease calls a careful MRC programmer would write by hand -- there is no runtime scanning or garbage-collection pass involved.

What happens to a weak reference the instant the object it points to is deallocated? A. It is automatically set to nil by the runtime B. It becomes a dangling pointer, just like an unsafe_unretained reference C. It prevents the object from being deallocated at all D. It throws a compile-time error

A. It is automatically set to nil by the runtime — weak references are tracked by the runtime and are automatically zeroed to nil the instant the object they referred to is deallocated, which is what makes them safe to message afterward (messaging nil is well-defined and harmless in Objective-C).

Which ownership qualifier does NOT keep an object alive and also does NOT automatically become nil when that object is deallocated? A. strong B. weak C. unsafe_unretained D. autoreleasing

C. unsafe_unretained — unsafe_unretained does not contribute to the retain count (like weak), but unlike weak it is not tracked by the runtime, so it is left dangling rather than zeroed when the object it pointed to is deallocated.

After casting a CFStringRef to an NSString * using __bridge_transfer, what should the calling code do next? A. Call CFRelease on the original CFStringRef, because __bridge_transfer does not transfer ownership B. Call CFRetain to balance the cast C. Do nothing further -- ARC now owns the resulting NSString * and will release it when its strong scope ends D. Manually set the CFStringRef to NULL to avoid a warning

C. Do nothing further -- ARC now owns the resulting NSString * and will release it when its strong scope ends — __bridge_transfer specifically hands the object's existing ownership over to ARC. Calling CFRelease afterward would double-release it, since ARC is now also going to release it.

Why can ARC not automatically prevent or break a retain cycle between two objects that hold strong references to each other? A. ARC does not support strong references between two custom classes B. ARC's compile-time analysis has no mechanism to detect that a cycle exists at runtime, since it inserts release calls based on static scope, not a live graph scan C. Retain cycles are actually impossible under ARC by design D. ARC only manages memory for NSString and NSArray, not custom classes

B. ARC's compile-time analysis has no mechanism to detect that a cycle exists at runtime, since it inserts release calls based on static scope, not a live graph scan — ARC performs a compile-time transformation, not a runtime reachability scan, so it has no mechanism analogous to a tracing garbage collector's cycle detection; two objects strongly retaining each other will simply never reach a retain count of zero.

What is the implicit ownership qualifier ARC applies to the pointee of an NSError ** out-parameter, and why? A. __strong, so the caller permanently owns the error object B. __unsafe_unretained, because errors are expected to be short-lived C. __autoreleasing, so the object handed back survives long enough for the caller to read it without the method itself retaining it long-term D. __weak, so the error automatically becomes nil after being logged

C. __autoreleasing, so the object handed back survives long enough for the caller to read it without the method itself retaining it long-term — ARC implicitly treats the pointee of an id*/NSError** out-parameter as __autoreleasing, meaning the object assigned into it is autoreleased -- alive at least until the next pool drain, which is long enough for the calling code's error-handling logic to read it.

A Timer class holds a strong property target of type id, and calls a method on target periodically. A ViewController creates a Timer and stores it in a strong property, and sets self as the timer's target. Explain the retain cycle this creates, and rewrite the target property's declaration to fix it.

The cycle: ViewController holds a strong reference to Timer (Timer's retain count depends on ViewController staying alive), and Timer holds a strong reference back to target, which is the same ViewController (ViewController's retain count depends on Timer staying alive). Neither retain count can ever reach zero, so neither object is ever deallocated, even after the screen holding the ViewController is dismissed. Fix: @property (nonatomic, weak) id target; -- the timer can still message its target, but no longer keeps it alive, breaking the cycle.

Trace the ownership in both directions: ViewController -> Timer, and Timer -> target (ViewController). If both are strong, neither can ever reach a retain count of zero.