BS
BleepingSwift
Published on

> Understanding Retain Cycles in Objective-C

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

In Objective-C's reference counting world, objects stay alive as long as someone holds a strong reference. When two objects reference each other, neither can be deallocated—even when nothing else needs them. This is a retain cycle, and it's probably responsible for most memory leaks in Objective-C apps.

How Reference Counting Works

Each object has a retain count. When you create or retain an object, the count increases. When you release it, the count decreases. When it hits zero, the object deallocates.

NSObject *obj = [[NSObject alloc] init]; // retain count: 1
[obj retain];                             // retain count: 2
[obj release];                            // retain count: 1
[obj release];                            // retain count: 0, deallocated

Under ARC, the compiler inserts these calls automatically. You don't write them, but they still happen.

The Cycle Problem

Consider two objects that reference each other:

@interface Person : NSObject
@property (nonatomic, strong) Apartment *apartment;
@end

@interface Apartment : NSObject
@property (nonatomic, strong) Person *tenant;
@end

When you connect them:

Person *john = [[Person alloc] init];      // john: 1
Apartment *unit = [[Apartment alloc] init]; // unit: 1

john.apartment = unit;  // unit: 2
unit.tenant = john;     // john: 2

Now set both variables to nil:

john = nil;  // john's count: 2 -> 1 (apartment still holds it)
unit = nil;  // unit's count: 2 -> 1 (john still holds it)

Both objects still have retain count 1. Neither deallocates. Memory leaked permanently.

Breaking Cycles with Weak References

The solution is making one reference weak instead of strong:

@interface Apartment : NSObject
@property (nonatomic, weak) Person *tenant;  // Changed to weak
@end

Weak references don't increment the retain count:

Person *john = [[Person alloc] init];       // john: 1
Apartment *unit = [[Apartment alloc] init]; // unit: 1

john.apartment = unit;  // unit: 2
unit.tenant = john;     // john: 1 (weak doesn't retain)

john = nil;  // john: 0 -> deallocated
             // When john deallocates, unit.tenant automatically becomes nil
unit = nil;  // unit: 1 -> 0 -> deallocated

The cycle is broken. Both objects deallocate properly.

Parent-Child Relationships

The general rule: parents own children strongly; children reference parents weakly.

@interface Parent : NSObject
@property (nonatomic, strong) NSArray<Child *> *children;  // Strong
@end

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

This pattern works for view hierarchies, data structures, delegate relationships, and more.

Block Retain Cycles

Blocks capture variables by reference, which can create cycles:

@interface DataLoader : NSObject
@property (nonatomic, copy) void (^completion)(NSData *);
@end

@implementation DataLoader

- (void)loadWithCompletion:(void (^)(NSData *))block {
    self.completion = block;
    [self startLoading];
}

- (void)startLoading {
    // When done:
    self.completion(loadedData);  // Uses self
}

@end

If you use it like this:

loader.completion = ^(NSData *data) {
    [self processData:data];  // Captures self strongly
};

The loader holds the block, the block holds self, and if self holds the loader, you have a cycle.

The weak-strong Dance

The canonical solution uses a weak reference to break the cycle:

__weak typeof(self) weakSelf = self;
loader.completion = ^(NSData *data) {
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return;  // Self was deallocated

    [strongSelf processData:data];
};

The weak capture prevents the cycle. The strong inside the block ensures self doesn't deallocate mid-execution.

Detecting Retain Cycles

Xcode's Memory Graph Debugger visualizes object relationships. Run your app, click the memory graph button, and look for cycles—objects connected in loops.

Instruments' Leaks tool also catches many retain cycles. It shows leaked objects and their retain history.

For manual debugging, override dealloc and log:

- (void)dealloc {
    NSLog(@"%@ deallocated", self);
}

If dealloc never fires when you expect it to, you likely have a cycle.

Common Cycle Sources

Delegates are frequently cycle sources if made strong:

@property (nonatomic, strong) id<SomeDelegate> delegate;  // Wrong - causes cycles

@property (nonatomic, weak) id<SomeDelegate> delegate;    // Correct

NSTimer retains its target until invalidated:

// This timer retains self, preventing deallocation
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                              target:self
                                            selector:@selector(tick)
                                            userInfo:nil
                                             repeats:YES];

You must call [self.timer invalidate] explicitly, typically in a method like viewWillDisappear: rather than dealloc (since dealloc won't be called while the cycle exists).

Notification observers with block handlers can capture self:

// Cycle if self holds observer and block holds self
self.observer = [[NSNotificationCenter defaultCenter]
    addObserverForName:@"SomeNotification"
                object:nil
                 queue:nil
            usingBlock:^(NSNotification *note) {
                [self handleNotification];  // Captures self
            }];

Use the weak-strong dance here too.

Unsafe Unretained

There's also __unsafe_unretained, which is like weak but doesn't nil out when the object deallocates. It's faster but dangerous—accessing a deallocated object crashes.

@property (nonatomic, unsafe_unretained) id unsafeRef;

Only use this when you're certain the referenced object outlives the reference, and you need the tiny performance gain. Weak is almost always the right choice.

Retain cycles are subtle but predictable. Follow the parent-child ownership rule, use weak for delegates and back-references, and capture self weakly in blocks. Your memory graph will thank you.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.