Break Objective-C Block Retain Cycles With __weak and __strong
Capture self weakly in blocks and promote it strongly inside to avoid retain cycles under ARC.
04 Apr 2026, 05:42 UTC

The problem: a block keeps its owner alive
Under Automatic Reference Counting (ARC), the compiler inserts retain, release, and autorelease calls for you. A retain cycle happens when two objects hold strong references to each other, so neither can ever be deallocated. The most common Objective-C case is a block stored on an object that also captures that object.
For example, if a view controller assigns a completion block to its own property, and that block references self, the view controller retains the block and the block retains the view controller. The object leaks even after the user leaves the screen.
Useful takeaway
Capture self as a zeroing weak reference before the block, then promote it to a strong temporary inside the block. Use that strong reference for the work, and check for nil in case the owner was already deallocated.
How ARC creates the cycle
ARC manages memory through object ownership. A strong property or variable increments an object's retain count; when the last strong reference disappears, ARC releases the object. A block captures variables by value by default. If the block captures self strongly, it becomes an owner. When the object owns that block through a property, ownership circles back to itself.
Worked example: weak capture and strong promotion
Assume a class ImageLoader that stores a completion block and calls it when a download finishes.
@interface ImageLoader : NSObject
@property (nonatomic, copy, nullable) void (^completion)(UIImage * _Nullable image);
@end
@implementation ImageLoader
- (void)startLoadWithCompletion:(void (^ _Nullable)(UIImage * _Nullable))completion {
self.completion = completion;
// Problem: this block captures self strongly.
self.completion = ^(UIImage * _Nullable image) {
[self cacheImage:image]; // self is retained by the block
};
}
@end
Because self owns self.completion and the block owns self, the loader stays alive forever.
The fixed version captures a weak reference and promotes it to strong inside the block:
- (void)startLoadWithCompletion:(void (^ _Nullable)(UIImage * _Nullable))completion {
self.completion = completion;
__block __weak ImageLoader * _Nullable weakSelf = self;
self.completion = ^(UIImage * _Nullable image) {
__strong ImageLoader * _Nullable strongSelf = weakSelf;
if (strongSelf) {
[strongSelf cacheImage:image];
}
};
}
__weak tells ARC not to retain the object and to set the variable to nil when the object is deallocated. __block lets the block refer to the variable itself rather than a copy of its value. __strong creates a temporary strong reference that keeps the loader alive for the rest of the block. If the loader was already released, strongSelf is nil and the cache call is skipped.
Core Foundation ownership is a separate decision
When bridging between Objective-C objects and Core Foundation types, ARC does not manage the Core Foundation side. A __bridge cast transfers no ownership. Use __bridge_retained when you create a Core Foundation object and want ARC to hand ownership to you, and __bridge_transfer when you want ARC to take ownership of a Core Foundation object you created. Mixing these up causes leaks or over-release crashes.
Limits and common mistakes
- Weak references can disappear mid-block. Always copy to a strong local before using the object across multiple statements. Do not use
weakSelfdirectly after the first line. - Weak capture means the work may never run. If the loader must stay alive until the download completes, a weak reference is the wrong tool. Keep a separate strong owner or redesign so the block does not own its owner.
- Do not mix manual memory management with ARC in one file. Compile ARC files with
-fobjc-arc. For legacy manual retain/release files, disable ARC per file with-fno-objc-arc. Mismatched ownership causes leaks or crashes. - Zeroing weak references require modern targets.
__weakrequires iOS 5 or macOS 10.7 and later. On older targets use__unsafe_unretained, which does not set the variable tonil. - Blocks start on the stack. A block property declared
copymoves the block to the heap. If you store a block without copying, it may point to stack memory that no longer exists.
Verification
Build with ARC enabled. In Xcode this is the default for new projects; from the command line, compile with clang -fobjc-arc.
- Add a log statement to
dealloc:
- (void)dealloc {
NSLog(@"%s: %p", __PRETTY_FUNCTION__, (void *)self);
}
- Run the load path, release the owner, and wait for the completion block to fire.
- Check that
deallocprints. If it does not, the block still holds the object. - Profile with Instruments > Leaks and Allocations while triggering the block path. The loader should not appear as a leaked block after its owner is released.
- Enable Address Sanitizer in the scheme to catch use-after-free if a promoted strong reference is used after deallocation.
Risks: Leaks can report autoreleased objects that have not yet drained, so compare results before and after the fix rather than relying on a single run. Address Sanitizer increases memory use and may not catch logical retain cycles that never crash.
Practical result to check
After the fix, releasing the owner should print the dealloc log even if the completion block is still scheduled. The block runs safely if the object is alive, and exits cleanly if it is not.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.