Fixing Objective-C Retain Cycles with ARC Weak References
ARC doesn't prevent every leak. Learn how strong block captures create retain cycles and how weak self breaks them.
17 Jul 2026, 08:54 UTC

The problem: your object never deallocates
You enabled Automatic Reference Counting (ARC), yet a view controller or model object stays in memory after it should be released. The usual culprit is a retain cycle: object A holds a strong reference to object B, and B holds a strong reference back to A. ARC balances each individual retain and release, but it cannot see that the two objects will never reach zero because they keep each other alive.
Takeaway: under ARC, you still own the object graph. Break cycles by making at least one edge weak, most commonly by capturing self weakly inside a block.
How ARC decides when to release an object
ARC is a compile-time feature of Clang/LLVM, not a garbage collector running at runtime. The compiler inserts retain and release calls based on ownership qualifiers:
- strong: the default for properties and local variables. Keeps the object alive as long as the reference exists.
- weak: does not keep the object alive. If the referenced object is deallocated, the weak variable is set to
nilautomatically. - unsafe_unretained: does not keep the object alive, but is not zeroed. Accessing it after deallocation is undefined behavior.
These qualifiers apply to Objective-C objects. Plain C pointers and Core Foundation types are not managed by ARC unless you use bridging casts.
Worked example: a block that captures self
Imagine a view controller that starts an asynchronous operation and supplies a completion block:
#import <UIKit/UIKit.h>
@interface NetworkController : NSObject
- (void)fetchDataWithCompletion:(void (^)(NSData *data, NSError *error))completion;
@end
@implementation MyViewController {
NetworkController *networkController;
}
- (void)loadData {
[networkController fetchDataWithCompletion:^(NSData *data, NSError *error) {
self.dataLabel.text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}];
}
@end
When loadData runs, the block literal captures self strongly by default. The view controller owns the network controller, the network controller owns the block (or the underlying operation does), and the block owns the view controller back. Even after the view controller is removed from the window, its reference count cannot reach zero.
Break the cycle with a weak reference to self
Declare a weak variable before the block and use it inside:
- (void)loadData {
__weak MyViewController *weakSelf = self;
[networkController fetchDataWithCompletion:^(NSData *data, NSError *error) {
__strong MyViewController *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
strongSelf.dataLabel.text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}];
}
__weak tells ARC not to count this reference. weakSelf becomes nil if the view controller is deallocated while the operation is still pending. Inside the block, __strong promotes the weak reference to a strong one for the duration of the callback, so the object cannot disappear mid-execution. If it is already nil, the block exits early.
Compile and verify the ARC build
Run this from the directory containing your source file. Replace MyViewController.m with your actual filename:
clang -fobjc-arc -fmodules -x objective-c MyViewController.m -o MyViewController
Where to run: Terminal on macOS with the Xcode command-line tools installed. No special permissions are required for compilation, only read access to the source files and write access to the output directory.
Expected checks: The compiler should emit no warnings about implicit retain/release or missing ownership qualifiers. If you see implicit conversion of 'int' to 'MyViewController *' or similar, check that your block capture uses __weak consistently.
Risks: Compiling without -fobjc-arc in a mixed project can leave manual retain/release calls unbalanced. Do not add manual memory management calls to ARC-compiled files.
Limits and common mistakes
ARC is compile-time only
ARC works with Clang and LLVM toolchains. Older compilers or pure C translation units bypass ARC checks entirely. If you compile the same file with and without -fobjc-arc in different targets, the object layout is compatible, but ownership semantics differ and can produce crashes at the boundary.
Do not mix manual retain/release with ARC
Calling retain, release, or autorelease directly in ARC code is a compiler error in most contexts. Attempting to work around it with compiler flags or casting can corrupt the hidden reference count stored with each object and cause crashes or leaks.
Weak references are not a universal fix
A weak reference must be used consistently. If a delegate property is declared unsafe_unretained instead of weak, the delegate can be deallocated while still assigned, and later messages to it crash. Similarly, storing a weak reference in a C struct or a global variable defeats ARC's zeroing behavior because the runtime cannot track it.
Blocks and retain cycles beyond self
Capturing any object strongly inside a block can create a cycle if that object eventually owns the block. Review every captured variable, not just self. If you capture a strong object that holds the operation queue holding the block, you have the same problem.
Practical way to check the result
Build with Xcode, enable Build Settings → Enable Automatic Reference Counting, and run the scenario under Instruments > Leaks. Remove the view controller from the navigation stack or dismiss the modal. If the object deallocates, its dealloc method runs and no leak is reported. If dealloc never runs, inspect every block and delegate property for strong captures.
You can also add a temporary log to dealloc:
- (void)dealloc {
NSLog(@"%s deallocated", NSStringFromClass([self class]));
}
Remove the log before shipping. Its absence after the expected release point confirms the cycle is still present.
Bottom line
ARC removes manual memory management for most code, but it does not eliminate retain cycles. Use __weak for back-references and delegate properties, promote to __strong inside blocks when you need the object to stay alive during the callback, and verify deallocation with dealloc logging or Instruments rather than assuming ARC handled it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.