Objective-C
automatic reference counting
memory leaks
ARC limitations
software development

What kind of leaks does automatic reference counting in Objective-C not prevent or minimize?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Automatic Reference Counting (ARC) automates retain and release calls for Objective-C objects, eliminating most manual memory management bugs. However, ARC cannot detect or break retain cycles (two objects holding strong references to each other), cannot manage non-object resources (file handles, sockets, C allocations), and cannot handle Core Foundation objects without explicit bridging. These categories of leaks require manual intervention even under ARC.

Retain Cycles (The Primary ARC Weakness)

ARC increments and decrements reference counts, deallocating objects when the count reaches zero. But when two objects reference each other, neither count ever reaches zero:

objc
1@interface Person : NSObject
2@property (strong, nonatomic) Person *partner;
3@end
4
5Person *alice = [[Person alloc] init];
6Person *bob = [[Person alloc] init];
7
8alice.partner = bob;   // bob's refcount = 2
9bob.partner = alice;   // alice's refcount = 2
10
11alice = nil;  // alice's refcount drops to 1 (bob still holds it)
12bob = nil;    // bob's refcount drops to 1 (alice still holds it)
13
14// Both objects leak. Neither is deallocated

Fix: Use weak for one side of the relationship:

objc
@interface Person : NSObject
@property (weak, nonatomic) Person *partner;  // Weak breaks the cycle
@end

Delegate Retain Cycles

The classic retain cycle pattern in iOS occurs when an object holds a strong reference to its delegate, and the delegate holds the object:

objc
1@interface NetworkManager : NSObject
2@property (strong, nonatomic) id<NetworkDelegate> delegate;  // WRONG: strong
3@end
4
5@interface ViewController : UIViewController <NetworkDelegate>
6@property (strong, nonatomic) NetworkManager *networkManager;
7@end
8
9// ViewController → strong → NetworkManager → strong → ViewController = CYCLE

Fix: Always declare delegates as weak:

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

Block Retain Cycles

Blocks (closures) capture variables by strong reference. If an object stores a block that captures self, a cycle forms:

objc
1@interface DataLoader : NSObject
2@property (copy, nonatomic) void (^completionHandler)(NSData *);
3@end
4
5@implementation DataLoader
6- (void)startLoading {
7    self.completionHandler = ^(NSData *data) {
8        // This block captures 'self' strongly
9        [self processData:data];
10    };
11    // self → completionHandler block → self = CYCLE
12}
13@end

Fix: Use __weak to break the cycle:

objc
1- (void)startLoading {
2    __weak typeof(self) weakSelf = self;
3    self.completionHandler = ^(NSData *data) {
4        __strong typeof(weakSelf) strongSelf = weakSelf;
5        if (strongSelf) {
6            [strongSelf processData:data];
7        }
8    };
9}

The weakSelf/strongSelf dance prevents the cycle while ensuring self is alive during block execution.

Timer Retain Cycles

NSTimer retains its target, creating a cycle if the target also retains the timer:

objc
1@interface MyViewController : UIViewController
2@property (strong, nonatomic) NSTimer *updateTimer;
3@end
4
5@implementation MyViewController
6- (void)viewDidAppear:(BOOL)animated {
7    [super viewDidAppear:animated];
8    // NSTimer retains self (target)
9    self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
10                                                       target:self
11                                                     selector:@selector(update)
12                                                     userInfo:nil
13                                                      repeats:YES];
14    // self → updateTimer → (runloop retains timer) → timer retains self = CYCLE
15}
16@end

Fix: Invalidate the timer before deallocation, or use a block-based timer (iOS 10+):

objc
1// Block-based timer with weak self
2__weak typeof(self) weakSelf = self;
3self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
4                                                   repeats:YES
5                                                     block:^(NSTimer *timer) {
6    [weakSelf update];
7}];
8
9// Always invalidate in viewWillDisappear
10- (void)viewWillDisappear:(BOOL)animated {
11    [super viewWillDisappear:animated];
12    [self.updateTimer invalidate];
13}

Core Foundation Objects

ARC only manages Objective-C objects (inheriting from NSObject). Core Foundation types (CFStringRef, CGImageRef, CFDictionaryRef) are not managed:

objc
1- (void)leakyCFFunction {
2    CFStringRef str = CFStringCreateWithCString(NULL, "hello", kCFStringEncodingUTF8);
3    // ARC does NOT release this. It is a CF object, not an ObjC object
4    // Must manually call CFRelease:
5    CFRelease(str);
6}
7
8// Bridging CF to ObjC transfers ownership to ARC:
9CFStringRef cfStr = CFStringCreateWithCString(NULL, "hello", kCFStringEncodingUTF8);
10NSString *nsStr = (__bridge_transfer NSString *)cfStr;  // ARC takes ownership
11// No CFRelease needed because ARC manages nsStr

C Memory Allocations

malloc, calloc, mmap, and other C allocations are invisible to ARC:

objc
1- (void)processData {
2    char *buffer = malloc(1024);
3    // Do work with buffer...
4
5    // ARC cannot free this. You must call free() manually
6    free(buffer);
7}

Observer and Notification Leaks

Registering as an observer without removing creates a dangling reference (not a leak per se, but a crash or zombie access):

objc
1// This does NOT leak under ARC (NSNotificationCenter uses weak refs since iOS 9)
2// But KVO observers must still be removed:
3- (void)viewDidLoad {
4    [self.model addObserver:self forKeyPath:@"status" options:0 context:nil];
5}
6
7- (void)dealloc {
8    // MUST remove KVO observer. ARC does not do this
9    [self.model removeObserver:self forKeyPath:@"status"];
10}

Threads that retain objects on their stack can prevent deallocation:

objc
1@property (strong, nonatomic) NSThread *backgroundThread;
2
3// If the thread runs indefinitely and retains self, it's a cycle
4self.backgroundThread = [[NSThread alloc] initWithTarget:self
5                                                selector:@selector(runLoop)
6                                                  object:nil];
7[self.backgroundThread start];
8// NSThread retains target (self) until the thread exits

Common Pitfalls

  • Assuming ARC prevents all leaks: ARC only automates retain/release. Retain cycles, CF objects, and C allocations still leak. Use Instruments (Leaks template) to find them.
  • Forgetting __weak in blocks: Any block stored as a property that references self creates a cycle. Always use __weak typeof(self) weakSelf = self for stored blocks.
  • weak vs assign: Use weak for object properties (zeroes out on deallocation). assign for primitives. Using assign for objects creates a dangling pointer.
  • Bridging without transfer: (__bridge NSString *)cfStr does not transfer ownership, so you still own the CF object. Use __bridge_transfer (or CFBridgingRelease) to hand ownership to ARC.
  • NSTimer in viewDidLoad without invalidation: The timer retains self, preventing dealloc from ever being called. Without dealloc, the timer is never invalidated, creating a deadlock. Invalidate in viewWillDisappear or use block-based timers.

Summary

  • ARC does not break retain cycles. Use weak references for delegates, parent pointers, and one side of bidirectional relationships
  • Use __weak / strongSelf pattern in blocks that are stored as properties
  • Core Foundation objects require manual CFRelease or __bridge_transfer to ARC
  • C allocations (malloc, calloc) require manual free()
  • NSTimer retains its target. Invalidate timers explicitly or use block-based timers
  • Use Instruments Leaks and Zombies to detect ARC-invisible memory issues

Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions