Automatic Reference Counting

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, instancetype, Clang modules, lightweight generics, nullability and @available — as published at Apple Developer Documentation and the Clang Objective-C specifications, which are the references these pages are written and verified against.

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 compile-time automation of the reference-counting rules described in Manual Retain Release. The compiler analyses the lifetime of every object pointer and inserts the retain, release and autorelease calls you would otherwise have written — on every path, including early returns and exception unwinds. It is not garbage collection: there is no background collector, no pauses and no nondeterminism. Deallocation happens at exactly the moment the last strong reference disappears.

What ARC Does and Does Not Do

ARC does:

  • Insert retain/release/autorelease for every Objective-C object pointer.

  • Optimise away pairs that provably cancel, making ARC code typically faster than hand-written MRR.

  • Zero __weak references automatically when their object is deallocated.

  • Emit [super dealloc] and release your ivars for you.

  • Enforce the naming conventions, so it can reason about ownership across method boundaries.

ARC does not:

  • Break retain cycles. Two objects that strongly reference each other are never deallocated. This is the one memory bug ARC does not solve, and the rest of this page is largely about avoiding it.

  • Manage Core Foundation objects (CFRelease is still yours), malloc/free, or file descriptors and other non-memory resources.

  • Manage struct fields containing object pointers without explicit qualification.

  • Make code thread-safe.

Under ARC you may not call retain, release, autorelease or retainCount at all — they are compile errors — nor override retain/release. @autoreleasepool remains, and remains occasionally necessary.

Ownership Qualifiers

Every object pointer variable has one of four lifetime qualifiers. __strong is the default, so most code never writes one.

Qualifier Behaviour

__strong

The default. Keeps the object alive; assigning releases the old value and retains the new one.

__weak

Does not keep the object alive, and is set to nil automatically when the object is deallocated. The tool for breaking cycles and for back-references.

__unsafe_unretained

Does not keep the object alive and is not zeroed — it dangles after deallocation. Only for the few classes that do not support weak references, or for measured performance work.

__autoreleasing

Used for out-parameters passed by reference (NSError **). Rarely written by hand; the compiler applies it.

__strong Person *owner = [[Person alloc] initWithName:@"Ada"];   // "__strong" is redundant
__weak   Person *observer = owner;

NSLog(@"%@", observer);        // the object
owner = nil;                   // last strong reference gone -> deallocated
NSLog(@"%@", observer);        // (null) -- automatically zeroed

Demonstrate this with an instance of your own class, not a short NSString. Small strings and boxed numbers are often tagged pointers — the value is carried in the pointer itself, nothing is heap-allocated, and so the object is never deallocated and a weak reference to it is never zeroed. The same code written with [[NSString alloc] initWithFormat:@"%d", 42] prints 42 twice, which looks like weak is broken when it is the example that is wrong.

Note the qualifier’s position for pointer-to-pointer types — it binds to the inner pointer:

NSError * __autoreleasing *errorPtr;      // correct
// __autoreleasing NSError **errorPtr;    // means something else

The property attributes strong, weak, assign/unsafe_unretained and copy map onto these qualifiers exactly — see Properties and Encapsulation.

A Critical __weak Subtlety

A __weak variable can become nil between two statements, because another thread may have released the object in between. Read it once into a strong local before using it more than once:

// Risky: weakObj may be non-nil at the check and nil at the call.
if (weakObj) {
    [weakObj doSomething];
}

// Correct: one atomic load into a strong reference.
__strong typeof(weakObj) strongObj = weakObj;
if (strongObj) {
    [strongObj doSomething];
    [strongObj doSomethingElse];     // guaranteed to be the same live object
}

This is the same "weak/strong dance" that blocks require — see Blocks.

Method Families and Naming Conventions

ARC decides who owns a returned object from the method’s name. A method whose first word is alloc, new, copy, mutableCopy or init belongs to an ownership family and returns an object the caller owns; anything else returns an autoreleased object.

This is why naming is a correctness issue, not a style one:

- (NSString *)newIdentifier;        // "new..." family: caller takes ownership
- (NSString *)generatedIdentifier;  // not a family: returns autoreleased

If you must break the convention — usually when wrapping a C API — annotate explicitly:

Attribute Meaning

NS_RETURNS_RETAINED

Returns an object the caller owns, despite the name.

NS_RETURNS_NOT_RETAINED

Returns an autoreleased object, despite an ownership-family name.

NS_RETURNS_INNER_POINTER

Returns a pointer into the receiver’s storage; keep the receiver alive.

NS_CONSUMED (on a parameter)

The method takes ownership of this argument.

NS_CONSUMES_SELF

The method takes ownership of the receiver.

- (NSString *)newString NS_RETURNS_NOT_RETAINED;           // in the `new` family, but autoreleased
- (void)takeOwnershipOf:(NS_CONSUMED id)object;

init methods have two further rules ARC enforces: they must return instancetype or id, and they must assign self = [super init…] — see Classes and Objects.

Retain Cycles

A cycle is any loop of strong references. ARC counts correctly; the count simply never reaches zero.

A retain cycle between a parent and child object where both references are strong, and the same pair with the back-reference made weak so the cycle is broken

The Three Shapes

1. Parent and child.

@interface Parent : NSObject
@property (nonatomic, strong) Child *child;          // parent owns child
@end

@interface Child : NSObject
@property (nonatomic, weak) Parent *parent;          // back-reference: weak
@end

The rule is directional: the owner holds strong, the owned holds weak.

2. Delegates. The same shape, which is why every delegate property is weak:

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

3. Blocks capturing self. The most common cycle in modern code:

// Cycle: self owns the block (copy property), the block retains self.
self.completion = ^{ [self refresh]; };

// Broken:
__weak typeof(self) weakSelf = self;
self.completion = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) { return; }
    [strongSelf refresh];
};

Not every captured self is a cycle — dispatch_async, UIView animation blocks and enumeration blocks are all owned by something else and release the block after running. Applying weakSelf where it is not needed causes the opposite bug: an object deallocated before its callback fires. See Blocks.

NSTimer deserves a special mention: a repeating timer holds its target strongly, so weak on your side does not help. Invalidate it explicitly, or use the block-based +scheduledTimerWithTimeInterval:repeats:block: with a weak capture.

dealloc Under ARC

- (void)dealloc {
    // Release only what ARC does not know about:
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [self removeObserver:self forKeyPath:@"state"];    // KVO observers
    [_timer invalidate];
    CFRelease(_cfObject);                              // Core Foundation
    free(_buffer);                                     // malloc'd memory

    // No ivar releases -- ARC does those.
    // No [super dealloc] -- ARC emits it. Writing it is a compile error.
}

Access ivars directly (_timer, not self.timer) and never let self escape from dealloc — registering it somewhere at this point resurrects a half-destroyed object.

@autoreleasepool

Still required in two places, exactly as under MRR:

// A loop creating many temporaries.
for (NSInteger i = 0; i < 1000000; i++) {
    @autoreleasepool {
        NSString *s = [NSString stringWithFormat:@"row %ld", (long)i];
        [self process:s];
    }                                    // drained each iteration -- flat memory use
}

// A thread you created yourself: only the main run loop provides one for free.
- (void)workerBody {
    @autoreleasepool {
        // … all work …
    }
}

If a batch job’s memory climbs steadily and Instruments shows autoreleased temporaries, a missing inner pool is almost always the reason.

Bridging Casts with Core Foundation

ARC manages Objective-C objects, not Core Foundation ones. At the boundary you must say what happens to ownership, and the cast is how you say it.

Cast Function equivalent Ownership

__bridge

 — 

No transfer. A borrowed reference; neither side changes its claim.

__bridge_retained

CFBridgingRetain()

ARC → you. You must CFRelease it.

__bridge_transfer

CFBridgingRelease()

You → ARC. Do not CFRelease it.

NSString *s = @"Hello";

// Borrow: just reading through a CF API.
CFStringRef borrowed = (__bridge CFStringRef)s;
CFIndex len = CFStringGetLength(borrowed);          // do NOT CFRelease(borrowed)

// Hand ownership to a CF API that will release it.
CFStringRef owned = (__bridge_retained CFStringRef)s;
CFRelease(owned);                                   // your responsibility now

// Take ownership of something a Create/Copy function returned.
CFStringRef created = CFStringCreateWithCString(NULL, "hi", kCFStringEncodingUTF8);
NSString *managed = (__bridge_transfer NSString *)created;   // ARC will release it
// do NOT CFRelease(created)

The decision procedure is CF’s own Create Rule: if the function’s name contains Create or Copy, you own the result, so use bridge_transfer (or CFBridgingRelease) to hand it to ARC. Otherwise it is borrowed (the Get Rule) and bridge is correct. CFBridgingRetain/CFBridgingRelease do the same jobs in function form and are often more readable in dense code.

Object Pointers in C Structs

Modern Apple Clang does accept an object pointer as a struct field — such a struct becomes a non-trivial C struct, and the compiler emits copy and destroy helpers so that ordinary declaration, assignment and scope exit manage the field’s lifetime correctly:

struct Managed {
    NSString *name;                        // fine: ARC manages this field
};

struct Manual {
    __unsafe_unretained NSString *name;    // you manage the lifetime yourself
};

What ARC cannot do is manage such a struct through raw memory. malloc hands back uninitialised bytes with no helper ever running, and memcpy/free bypass the helpers entirely, so a strong field in a heap-allocated struct is never retained or released:

struct Managed *s = malloc(sizeof *s);     // no copy/destroy helper runs
s->name = @"Ada";                          // not retained -- ARC never saw this storage
free(s);                                   // not released -- leaked or dangling

That is the case where __unsafe_unretained and a documented, hand-managed lifetime are required.

The right answer is nearly always a small Objective-C class, or an NSValue/NSDictionary, rather than the qualified struct.

Per-File ARC

ARC is a per-translation-unit setting, so mixed projects are normal:

clang -fobjc-arc          MyClass.m       # ARC on (the project default)
clang -fno-objc-arc       LegacyClass.m   # ARC off for this one file

In Xcode, set -fno-objc-arc (or -fobjc-arc) as a per-file flag under Build Phases ▸ Compile Sources. ARC and MRR files link and interoperate freely, because they share the same ownership conventions.

__has_feature(objc_arc) lets a header adapt:

#if __has_feature(objc_arc)
    #define MY_RELEASE(x)  do { (x) = nil; } while (0)
#else
    #define MY_RELEASE(x)  do { [(x) release]; (x) = nil; } while (0)
#endif

Diagnosing Leaks

Tool Finds

Clang Static Analyzer (xcodebuild analyze, or ⇧⌘B in Xcode)

Leaks, over-releases and mismatched bridging casts without running the code. Run it routinely — it is the cheapest of these by far.

Instruments ▸ Leaks

Blocks of memory with no remaining references — classic leaks, including cycles.

Instruments ▸ Allocations with Mark Generation

Steady growth. Mark a generation, perform an operation, return to the starting state, mark again: anything still alive in the new generation is suspect. The best tool for finding retain cycles.

Xcode Memory Graph Debugger (the graph icon while running)

A live object graph with cycles called out explicitly and purple ! warnings on leaked objects. Usually the fastest way to see a cycle.

Zombies (Instruments, or NSZombieEnabled)

Messages to deallocated objects — the over-release side, which still occurs at the CF boundary.

Address Sanitizer (-fsanitize=address)

Use-after-free and buffer overflows in the C parts of your code.

The practical routine: run the static analyzer on every build, use the Memory Graph Debugger when an object’s dealloc does not fire, and reach for Instruments' Allocations when memory grows without an obvious culprit. See Build and Tooling.

See Also