Blocks + Runtime + Memory: One Unified Mental Model
Synthesize three previously-separate topics — Blocks, the Objective-C Runtime, and ARC memory management — into a single mental model, and master the weak/strong dance that avoids retain cycles when a block captures self.
This course has taught Blocks, the Objective-C Runtime, and Memory Management (ARC and ownership qualifiers) as three separate modules, each with its own lessons and its own vocabulary. That separation was useful for learning each piece in isolation, but it hides a deeper truth: these are not three unrelated topics that happen to share a course. A block is a runtime object. Capturing a variable in a block is memory management, governed by the exact same ownership rules as an @property (strong). And the retain cycles that make blocks notorious are not a special 'block bug' — they're the same cycle-by-strong-reference problem covered in Memory Management, just triggered by a closure instead of an explicit assignment. This lesson exists specifically to draw those threads together, walk the full chain from block creation to potential retain cycle, and teach the weak/strong dance that Objective-C developers use to break that chain safely.
A Block Is a Runtime Object
The Objective-C Runtime lesson established that every object has an isa pointer identifying its class, and that class objects themselves carry method lists, property lists, and a place in a superclass chain — all runtime data structures accessible through functions like object_getClass and class_getName. Blocks are not exempt from any of this. A block literal, written as ^{ ... } or ^(NSInteger x) { return x * 2; }, compiles down to a real Objective-C object at runtime, backed by a real class — commonly __NSMallocBlock__, __NSStackBlock__, or __NSGlobalBlock__ depending on where and how it's allocated — complete with its own isa pointer, exactly like an instance of NSString or a custom class you've written.
You can confirm this directly with the tools from the Runtime Introspection lesson: calling object_getClass on a block instance returns an honest-to-goodness Class, and class_getName on it returns a real name like __NSMallocBlock__. This isn't a curiosity — it's the entire foundation this lesson is built on. Because a block is an object, everything you already know about how ARC manages objects — retain, release, strong references, weak references — applies to blocks exactly the same way it applies to any other object. There is no separate 'block memory management' rulebook; there's one rulebook, and blocks are subject to it because blocks are objects.
This is also why blocks can be copied, stored in properties, passed as parameters typed with a block signature, and passed around generally like any other Objective-C value — because under the hood, that's precisely what they are.
Capturing Object: How a Block Captures Its Environment
The single defining feature of a block, as covered in the Blocks module, is that it captures the variables from its surrounding scope at the point it's defined — it can read (and, for __block variables, write) values from the enclosing function even after that function has returned and its stack frame is long gone. This is what makes a block a genuine closure rather than just an anonymous function.
For an object-typed variable — anything held by a pointer to an Objective-C class, including self — capturing that variable means the block needs to keep the object alive for as long as the block itself exists, since the block might run at some arbitrary point in the future, long after the code that created it has moved on. The mechanism for 'keeping an object alive' in ARC is retaining it — taking a strong reference. And that is exactly what a block does by default: when a block captures an object-typed variable, the block takes a strong reference to that object, retaining it for the block's own lifetime.
This is the critical insight this lesson is built around: capturing an object strongly inside a block is not a special block-specific behavior bolted on top of ARC. It is ARC's ordinary strong-reference rule, applied to a new kind of holder — the block object itself — exactly the same as when a @property (strong) holds a reference to an object. A block's captured variables are, conceptually, just instance variables of the hidden block object the compiler generates, and by default those instance variables are strong, for exactly the same reason a @property (strong) Person *owner; is strong: because something needs to own the object and keep it alive.
Strong Capture, Object Retained: Where the Trouble Starts
Trouble begins with one extremely common, entirely natural pattern: a block defined inside a method, referencing self. self is almost always the object whose method is currently running, so it's completely ordinary for a block written inside one of self's methods to reference self directly, or to reference an instance variable or property of self (which implicitly captures self, since accessing an ivar requires knowing which object's ivar it is).
When that block strongly captures self, and that block is then stored somewhere self also strongly holds onto — most commonly, assigned to a property of self, like a completion-handler property, or added to a collection self owns — you now have two strong references pointing at each other: self strongly retains the block (through its property), and the block strongly retains self (through its capture). Neither object can ever be deallocated, because each one is precisely what's keeping the other's retain count above zero. This is the exact retain-cycle scenario from the Memory Management module, just with a block playing the role that, in that earlier lesson, was played by two plain objects holding strong references to each other.
It's worth being precise about when this actually causes a cycle, because not every self-capturing block is a leak. If the block is used and discarded immediately — passed straight to a method that calls it once and lets it go, like a typical animation completion handler that isn't stored anywhere long-term — there's no cycle, because nothing is holding onto the block itself once it finishes running. The danger is specifically when the block is *retained for a long or indefinite time* by an object that the block also strongly captures — most classically, a block stored in a property (self.completionHandler = ^{ ... self ... };) or captured into a long-lived array, timer, or notification observer.
The Weak/Strong Dance: Breaking the Cycle
The standard fix is to break one side of the cycle by making the block's reference to self weak instead of strong — a weak reference, as covered in Memory Management, doesn't retain the object it points to, so it can't keep it alive, and it automatically becomes nil if the object it points to is deallocated. The pattern for doing this correctly has two distinct steps, and both matter.
Step one happens *before* the block: create a weak-qualified local variable that points to self, and capture that variable in the block instead of capturing self directly — __weak typeof(self) weakSelf = self;, immediately before the block literal. Because the block now captures weakSelf (a weak reference) rather than self (which would be a strong capture), the block no longer keeps self alive, breaking the retain cycle at its source.
Step two happens as the *first line inside* the block body: re-strongify that weak reference into a new, temporary strong local variable — __strong typeof(self) strongSelf = weakSelf; — and use strongSelf (never weakSelf or self directly) for the rest of the block's body. This second step is not optional decoration; it solves a real, separate problem. Without it, if self happens to be deallocated by some other code partway through the block's execution — a real possibility, since a weak reference can turn nil at any moment control is yielded — later lines inside the block that keep re-reading weakSelf could see it flip to nil mid-execution, causing lines earlier in the block to see a live object and later lines to see nil, an inconsistent, hard-to-reproduce bug. Re-strongifying at the very top of the block takes one consistent, temporary strong reference that guarantees self stays alive for the full duration of that one block execution — but only for that duration, since strongSelf is a local variable that's released the moment the block returns, so it does not recreate the permanent retain cycle the weak capture was meant to avoid in the first place.
Modern Alternatives and Ergonomics
Swift, Objective-C's successor language, addresses the exact same underlying problem — a closure strongly capturing self — with more concise, built-in syntax: a capture list like { [weak self] in ... } lets Swift declare the weak capture directly in the closure's own syntax, and Swift's optional-binding idioms (guard let self = self else { return }) provide a terser equivalent of the re-strongify step, often collapsed into a single line at the top of the closure. This is a genuine language-ergonomics difference worth knowing about if you move between the two languages: the underlying problem — a closure capturing self strongly enough to create a retain cycle — is identical in both languages, because it stems from the same reference-counted memory model, but Objective-C's solution requires two explicit, hand-written variable declarations (__weak before, __strong inside) where Swift folds the equivalent into the closure's own capture-list syntax.
Because the weak/strong dance in Objective-C is boilerplate — the same two lines, nearly verbatim, at the top of countless blocks across a codebase — many real-world Objective-C codebases wrap it in a macro (commonly named something like weakify(self) / strongify(self), a convention popularized by libraries like libextobjc) to cut down on repetition and reduce the chance of a typo breaking the pattern. Recognizing this macro pattern when you encounter it in existing code is useful: underneath the macro, it's doing exactly the two-step __weak/__strong dance taught in this lesson, nothing more.
None of this changes the underlying mental model this lesson is built around: whether you write the weak/strong dance by hand, use a macro that expands to it, or write Swift's [weak self], the mechanism being managed is the same one — a block (or closure) is an object, capturing another object strongly retains it, and two objects strongly retaining each other never get deallocated. Every technique here exists purely to avoid creating that strong-strong cycle in the first place.
Putting the Three Threads Together
Step back and look at the full chain this lesson has walked: a block is a runtime object (Runtime), it captures variables from its enclosing scope including self (Blocks), and by default that capture is a strong reference subject to ARC's ordinary ownership rules (Memory Management) — which is exactly why a self-capturing block stored in a property of self can form a retain cycle, and exactly why weakly capturing self and re-strongifying inside the block breaks that cycle without needing any new concept beyond what those three modules already taught separately.
This is the payoff of treating these as one toolkit rather than three: once you see a block as 'just an object that happens to hold captured variables as something like its own ivars,' the entire weak/strong dance stops looking like a block-specific incantation to memorize, and starts looking like the same strong/weak reasoning you already apply to any two objects that might reference each other — because that's precisely what it is.
The Weak/Strong Dance
__weak breaks the retain cycle: the block no longer keeps self alive. __strong inside the block guarantees self stays alive for the block's full execution, but only for that execution. Always guard for nil — weakSelf (and therefore strongSelf) can legitimately be nil if self was already deallocated.
// Step 1 — BEFORE the block: create a weak local pointing to self.
__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
// Step 2 — FIRST line inside the block: re-strongify.
__strong typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return; // self was already deallocated; nothing to do
}
// Use strongSelf (never weakSelf or self) for the rest of the block.
[strongSelf handleCompletion];
};Code Examples
This confirms directly, using object_getClass and class_getName from the previous lesson, that a block literal is not special syntax that magically avoids the runtime — it compiles down to an instance of a real block class with its own isa pointer, exactly like any other Objective-C object.
NSLog(@"Block class: %s", class_getName(object_getClass(greet)));
self.completionHandler holds a strong reference to the block (because the property is declared copy/strong-equivalent for blocks). The block, in turn, references self.statusText, which implicitly captures self itself, and that capture is strong by default. self -> block -> self is a closed cycle of strong references, so ARC can never deallocate either object.
Common mistake: Capturing self directly and strongly inside a block that self itself retains long-term (here, via a stored property) — the textbook block retain cycle.
weakSelf breaks the cycle: the block captures a weak reference, so it no longer retains self, and self.completionHandler -> block -> self is no longer an unbroken chain of strong references. strongSelf, re-created fresh at the top of the block body, guarantees self stays alive and consistent for the rest of THIS execution of the block, without recreating a permanent strong capture — strongSelf is a local variable and is released the instant the block returns. Expected result: Load finished: Done
__strong typeof(self) strongSelf = weakSelf;
This macro pair expands to precisely the same __weak-before / __strong-inside pattern taught earlier in this lesson — it exists purely to remove repetition, not to introduce any new concept. Recognizing this pattern in real codebases (whether hand-written or macro-generated) is the practical payoff of understanding the mechanism underneath it. strongify's early return if (!var) return; mirrors the explicit nil-check in the earlier worked example — guarding against self having already been deallocated before the block ran.
// A block is a real runtime object — prove it with introspection tools
// from the Runtime Introspection lesson.
void (^greet)(void) = ^{
NSLog(@"Hello from a block!");
};
NSLog(@"Block class: %s", class_getName(object_getClass(greet)));
// Prints something like: Block class: __NSMallocBlock__@interface DataLoader : NSObject
@property (nonatomic, copy) void (^completionHandler)(void);
@property (nonatomic, strong) NSString *statusText;
- (void)loadData;
@end
@implementation DataLoader
- (void)loadData {
// BAD: this block strongly captures self, and self strongly
// retains this block via the completionHandler property.
self.completionHandler = ^{
self.statusText = @"Done"; // implicitly captures self strongly
NSLog(@"Load finished: %@", self.statusText);
};
// self -> completionHandler -> block -> self : a retain cycle.
// Neither object can ever reach a retain count of zero.
}
@end@implementation DataLoader
- (void)loadData {
// Step 1: weak capture, declared BEFORE the block.
__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
// Step 2: re-strongify as the FIRST line inside the block.
__strong typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return; // self is already gone; nothing safe to do
}
strongSelf.statusText = @"Done";
NSLog(@"Load finished: %@", strongSelf.statusText);
};
// self -> completionHandler -> block -> weakSelf (self)
// The last link is now weak: the cycle is broken.
}
@end// A macro-based weak/strong dance, the style many real codebases use
// to cut down on repeated boilerplate (conceptually like libextobjc's
// @weakify / @strongify).
#define weakify(var) __weak typeof(var) weak##var = var;
#define strongify(var) \
__strong typeof(var) var = weak##var; \
if (!var) return;
- (void)startDownload {
weakify(self);
self.completionHandler = ^{
strongify(self);
// 'self' below refers to the freshly re-strongified local —
// the macro has done exactly the two-step dance for you.
[self handleCompletion];
};
}How a Self-Capturing Block Can Become a Retain Cycle
A block captures a variable; if that variable is an object and the capture is strong (the default), the block retains it. If that retained object also strongly holds the block (e.g. via a property), neither can ever be deallocated — a possible retain cycle.
Block
↓
Captures Object
↓
Strong Capture
↓
Object Retained
↓
Possible CycleWhere This Pattern Is Unavoidable
The weak/strong dance shows up constantly in real Objective-C code wherever a long-lived object stores a block that references it: network request completion handlers stored as properties, notification observer blocks registered with NSNotificationCenter and never explicitly removed, animation completion handlers retained by a queue of pending work, and timer callback blocks are the most common real-world sites of exactly this cycle. Because the pattern recurs so often, recognizing it on sight — a block, stored somewhere self also owns, referencing self or any property of self — is one of the most practically valuable skills in production Objective-C code review; a huge share of real memory leaks in shipped Objective-C apps trace back to precisely this shape of bug.
self.completionHandler = ^{ [self doSomething]; };
__weak typeof(self) weakSelf = self; self.completionHandler = ^{ __strong typeof(self) strongSelf = weakSelf; [strongSelf doSomething]; };
Capturing self directly inside a block that self itself retains long-term (via a stored property here) creates a strong-strong retain cycle: self keeps the block alive, and the block keeps self alive, so neither is ever deallocated.
__weak typeof(self) weakSelf = self; self.completionHandler = ^{ [weakSelf doSomething]; [weakSelf doSomethingElse]; };
__weak typeof(self) weakSelf = self; self.completionHandler = ^{ __strong typeof(self) strongSelf = weakSelf; if (!strongSelf) return; [strongSelf doSomething]; [strongSelf doSomethingElse]; };
Using weakSelf directly throughout the block, without re-strongifying at the top, risks self being deallocated partway through the block's execution — weakSelf could turn nil between the two calls, so the first call runs against a live object while the second silently no-ops against nil, an inconsistent, hard-to-debug outcome.
__strong typeof(self) strongSelf = self; // declared where weakSelf should be, before the block
__weak typeof(self) weakSelf = self; // must be weak, and declared before the block
The variable declared BEFORE the block, that the block actually captures, must be __weak — declaring it __strong here defeats the entire purpose, since the block would once again strongly capture self (just through an extra variable), recreating the exact retain cycle the pattern exists to avoid.
// Treating every block that touches self as an automatic retain-cycle risk
// Recognizing that a block passed and used immediately (not stored long-term) rarely needs the weak/strong dance
The retain cycle specifically requires the block itself to be retained long-term by something the block also strongly captures (commonly a property of self). A block passed straight into a method that calls it once and discards it — like many one-shot animation completion blocks — is not stored anywhere and does not form a cycle, so reflexively weak-capturing self everywhere adds unnecessary complexity and unnecessary nil-checks.
What is a block, at the runtime level? A. A C function pointer with no object identity B. A real Objective-C object, backed by a class such as __NSMallocBlock__, with its own isa pointer C. A compiler macro that disappears entirely after compilation D. A protocol that objects can conform to
B. A real Objective-C object, backed by a class such as __NSMallocBlock__, with its own isa pointer — A block literal compiles down to an instance of a real block class (e.g. __NSMallocBlock__), with its own isa pointer — confirmable directly with object_getClass and class_getName from the Runtime Introspection lesson.
By default, when a block captures an object-typed variable, what happens? A. The block takes a weak reference automatically B. The block takes a strong reference, retaining the object for the block's lifetime — the same default ARC applies to a strong property C. The variable is copied by value, with no reference counting involved D. The block refuses to compile unless the capture is explicitly annotated
B. The block takes a strong reference, retaining the object for the block's lifetime — the same default ARC applies to a strong property — A block's default capture of an object is a strong capture, governed by the exact same ARC ownership rules as a strong property — this is why a block capturing self can extend self's lifetime.
In the weak/strong dance, why is __strong typeof(self) strongSelf = weakSelf; placed as the FIRST line inside the block body? A. It's just a stylistic convention with no functional purpose B. So self can't be deallocated out from under the block partway through its execution, while the temporary strongSelf still avoids recreating a permanent retain cycle C. It speeds up compilation D. It is required by the Objective-C compiler for all blocks
B. So self can't be deallocated out from under the block partway through its execution, while the temporary strongSelf still avoids recreating a permanent retain cycle — Re-strongifying at the top guarantees a single, consistent strong reference for the block's entire execution — protecting against self being deallocated mid-execution — while remaining temporary (a local variable released when the block returns), so it does not recreate the permanent strong-capture cycle the weak capture was meant to avoid.
What specifically causes a retain cycle when a block captures self strongly? A. Simply capturing self in any block always causes a cycle B. The cycle requires self to also strongly retain the block itself, commonly by storing it in a property — creating self -> block -> self C. Retain cycles with blocks are a myth; ARC prevents them automatically D. It only happens with __block variables, never with self
B. The cycle requires self to also strongly retain the block itself, commonly by storing it in a property — creating self -> block -> self — A cycle needs two strong references pointing at each other. Capturing self alone isn't sufficient — the cycle forms only when something self also strongly owns (typically a property) holds onto the block, closing the loop: self retains the block, and the block retains self.
How does Swift's [weak self] capture-list syntax compare to Objective-C's weak/strong dance? A. They solve unrelated problems in different memory models B. Swift's syntax is functionally identical in purpose but more concise, folding the weak capture (and often the re-strongify step, via guard let) directly into the closure syntax C. Objective-C's approach is strictly safer and Swift cannot avoid retain cycles D. Swift closures cannot capture self at all
B. Swift's syntax is functionally identical in purpose but more concise, folding the weak capture (and often the re-strongify step, via guard let) directly into the closure syntax — Both languages address the identical underlying problem — a closure strongly capturing self under a reference-counted memory model — but Swift's [weak self] capture-list syntax, combined with optional-binding idioms, expresses the equivalent of Objective-C's two-step __weak/__strong dance more concisely, directly in the closure's own syntax.
Why might a real codebase wrap the weak/strong dance in a macro like weakify/strongify? A. To change the underlying memory management behavior B. To reduce repeated boilerplate and typo risk, while expanding to exactly the same __weak-before / __strong-inside pattern C. Because the compiler requires macros for weak references D. To make the block capture strong instead of weak
B. To reduce repeated boilerplate and typo risk, while expanding to exactly the same __weak-before / __strong-inside pattern — The weak/strong dance is nearly identical boilerplate at the top of countless blocks across a codebase. A macro pair (in the style of libextobjc's @weakify/@strongify) reduces repetition and typo risk without changing the underlying mechanism — it still expands to the same __weak-before, __strong-inside pattern.
A class ImageDownloader has a property @property (copy) void (^onFinish)(UIImage *image); and a method -(void)download that sets self.onFinish to a block referencing self.cachedImage and calling [self notifyObservers]. Rewrite -(void)download using the full weak/strong dance, including a nil-check, and explain in one sentence why the nil-check is necessary even though weakSelf was captured correctly.
• (void)download { __weak typeof(self) weakSelf = self; self.onFinish = ^(UIImage *image) { __strong typeof(self) strongSelf = weakSelf; if (!strongSelf) { return; } strongSelf.cachedImage = image; [strongSelf notifyObservers]; }; }
Even with a correct weak capture, self could already be deallocated by the time the block actually runs (e.g. the download finishes after the owning view controller has been dismissed) — the nil-check handles that case safely instead of operating on a dangling strongSelf.