Avoiding Objective‑C Memory Leaks: A Practical Guide to ARC, Retain Cycles, and Weak References
Learn how ARC works, spot common retain‑cycle pitfalls, and break them with weak references. A practical delegate example, block handling, and a checklist keep your Objective‑C code leak‑free.
02 Oct 2025, 19:08 UTC

What Happens When Objects Never Go Away
In a typical iOS app, you expect objects to be deallocated when they’re no longer needed. Automatic Reference Counting (ARC) handles the heavy lifting of calling retain and release for you, but ARC is still a reference‑counting system. If two objects keep strong references to each other, the reference count never drops to zero, and both stay in memory forever. That’s a retain cycle or memory leak.
Leaked objects can cause UI sluggishness, increased memory footprint, and eventually app termination. Detecting them is easy with Xcode’s Instruments, but preventing them requires a clear understanding of ownership semantics.
ARC 101 – Ownership Semantics at a Glance
ARC automatically inserts retain and release calls at compile time. The developer’s job is to declare the intended ownership using property attributes and block capture qualifiers:
strong– default. The receiver keeps the object alive.weak– does not increase the retain count. The reference becomesnilautomatically when the object is deallocated.unowned– similar toweakbut does not zero out; accessing it after deallocation crashes.unsafe_unretained– no safety checks; must be used with caution.
When you declare a property, you decide how the object will be retained. Mis‑matching these semantics is the root of most leaks.
Common Retain‑Cycle Triggers
Delegate Relationships
Classes often expose a delegate property. The classic mistake is to make this property strong when the delegate usually owns the delegating object. For example:
@interface MyViewController : UIViewController
@property (nonatomic, strong) MyCustomView *customView; // controller owns view
@end
@interface MyCustomView : UIView
@property (nonatomic, strong) id<MyCustomViewDelegate> delegate; // view owns delegate
@end
Here, MyViewController owns MyCustomView and MyCustomView owns the controller back through delegate. Both objects’ reference counts stay >0, so neither is deallocated when the view controller is popped.
Blocks Capturing Self
Blocks are often used for callbacks. If a block captures self strongly, and the block is stored on self (or on an object that self strongly references), a cycle forms. Example:
self.completionBlock = ^{ NSLog(@"Finished: %@", self.title); };
Because completionBlock is a strong property on self, the block holds a strong reference to self, and self holds the block.
Unintended Strong References in Collections
Adding objects to collections (e.g., NSMutableArray) creates strong references. If the collection is a property of the same object that owns the items, you can inadvertently create a cycle.
Breaking Cycles with Weak References
Delegate Pattern Fix
The solution is to declare the delegate property weak so the delegating object does not keep the delegate alive:
@interface MyCustomView : UIView
@property (nonatomic, weak) id<MyCustomViewDelegate> delegate;
@end
Now, MyViewController owns the view, but the view only keeps a weak reference to the controller. When the controller is popped, its reference count drops to zero, and the view is deallocated as well.
Blocks and Self
To avoid block cycles, capture self weakly inside the block:
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"Finished: %@", strongSelf.title);
}
};
Here, the block holds a weak reference to self. When the controller is deallocated, weakSelf becomes nil, preventing the block from keeping the controller alive.
Practical Example: A Simple Table View Cell
Below is a minimal, illustrative example that shows both the problem and the fix. Replace <#CellDelegate#> with your actual protocol name.
// Cell.h
#import <UIKit/UIKit.h>
@protocol CellDelegate;
@interface Cell : UITableViewCell
@property (nonatomic, weak) id<CellDelegate> delegate; // weak fixes cycle
- (void)configureWithText:(NSString *)text;
@end
// Cell.m
#import <Cell.h>
@implementation Cell
- (void)configureWithText:(NSString *)text {
self.textLabel.text = text;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self.delegate respondsToSelector:@selector(cellDidSelect:)]) {
[self.delegate cellDidSelect:self];
}
}
@end
// ViewController.h
#import <UIKit/UIKit.h>
#import <Cell.h>
@interface ViewController : UIViewController <CellDelegate>
@end
// ViewController.m
#import <ViewController.h>
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
self.tableView.dataSource = self;
[self.view addSubview:self.tableView];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 10; }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
if (!cell) cell = [[Cell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
cell.delegate = self; // weak reference
[cell configureWithText:[NSString stringWithFormat:@"Row %ld", (long)indexPath.row]];
return cell;
}
#pragma mark - CellDelegate
- (void)cellDidSelect:(Cell *)cell {
NSLog(@"Cell selected: %@", cell.textLabel.text);
}
@end
Running this code in Xcode, then navigating away from ViewController and back, will show that the controller deallocates correctly. If you change delegate to strong, Instruments’ memory‑graph will reveal a cycle between ViewController and Cell.
Trade‑Offs and Limitations
- Over‑Weakening: Declaring a property weak when you actually need a strong reference can lead to the object being deallocated prematurely, causing
nilmessages or crashes. Always ensure there is at least one strong owner elsewhere. - Legacy Code: When mixing ARC with manual retain/release code, a stray
retainorreleasecan upset the reference count. Use static analyzer (Product > Analyze) to catch such mismatches. - Unowned vs. Weak:
unownedis useful when you know the object’s lifetime will outlast the reference, but it risks crashes if the assumption fails. Preferweakfor delegate patterns. - Performance: Weak references incur a small runtime cost because the runtime must track them for zeroing. This cost is negligible in most cases.
Actionable Checklist for Your Next Project
- Identify all strong property relationships between objects.
- Mark delegate properties as
weakunless the delegate must own the delegator. - When storing blocks on an object, capture
selfweakly inside the block. - Use Xcode’s Instruments memory‑graph to visualize potential cycles early.
- Run
Product > Analyzeregularly; the static analyzer flags many retain‑cycle patterns. - Document ownership expectations in code comments to aid future maintainers.
By following these guidelines, you’ll keep your app’s memory footprint lean, avoid subtle crashes, and make ARC work for you instead of against you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.