A Block in Objective-C is more than a closure — it is a full C struct with specific memory layout rules that determine how variables are captured and when the Block is deallocated.” Later I ran into retain cycles, wondered what __block actually did, and why local variables couldn’t be modified. That’s when I realized the underlying mechanism was far more complex. After reading the source code, I finally understood it.
Code 1: Block Underlying Structure
A Block is compiled into a C struct. Here’s the actual structure definition from the runtime source code:
// Block’s underlying structure (from objc runtime source)
struct Block_layout {
void *isa; // points to class (Stack/Malloc/Global)
volatile int32_t flags; // lifecycle management flags
int32_t reserved; // reserved
void (*invoke)(void *, ...); // function pointer to the Block’s code
struct Block_descriptor *descriptor; // metadata: size, copy/dispose helpers
// captured variables are stored AFTER this structure in memory
};
struct Block_descriptor {
uint32_t reserved; // reserved
uint32_t size; // total size of the Block
void (*copy_helper)(void *dst, const void *src);
void (*dispose_helper)(const void *src);
const char *signature; // type encoding string
};
// The isa pointer determines the Block’s type:
// __NSConcreteStackBlock → lives on the stack (default)
// __NSConcreteMallocBlock → lives on the heap (after copy)
// __NSConcreteGlobalBlock → global (no captures, like a static function)
// Demo: three Block types
int globalVar = 10;
// Global Block — captures nothing
void (^globalBlock)(void) = ^{
NSLog(@"I am a global block");
};
NSLog(@"Global Block class: %s", object_getClass((__bridge id)globalBlock));
// prints: __NSConcreteGlobalBlock
// Stack Block → captures a local variable
int localVar = 42;
void (^stackBlock)(void) = ^{
NSLog(@"captured: %d", localVar);
};
NSLog(@"Stack Block class: %s", object_getClass((__bridge id)stackBlock));
// prints: __NSConcreteStackBlock
// Heap Block → stack block copied to heap
void (^heapBlock)(void) = [stackBlock copy];
NSLog(@"Heap Block class: %s", object_getClass((__bridge id)heapBlock));
// prints: __NSConcreteMallocBlock
Code 2: clang Rewrite — See the Real C++ Output
The most direct way to understand Blocks is to compile Objective-C to C++ and read the generated code:
// main.m — the code you write
int main() {
int external = 10;
void (^myBlock)(void) = ^{
NSLog(@"external = %d", external);
};
myBlock();
return 0;
}
// Run: clang -rewrite-objc main.m -o main.cpp
// The generated C++ shows:
// 1. The Block captures “external” by VALUE (copied into the struct)
struct __main_block_impl_0 {
struct __block_impl impl; // base structure
struct __main_block_desc_0 *Desc; // metadata
int external; // CAPTURED VALUE stored here!
};
// 2. The Block’s actual code lives here
void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int external = __cself->external; // reads from captured copy
NSLog((NSString *)&__NSConstantStringImpl, external);
}
// 3. The constructor initializes the Block
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,
int _external, int flags=0)
: external(_external) { // copies external’s VALUE
impl.isa = &__NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
// KEY INSIGHT: external is stored by VALUE inside the struct.
// Modifying “external” inside the Block modifies the COPY, not the original.
Code 3: Variable Capture Comparison
Different variable types are captured differently. This is the #1 source of confusion in Block interviews:
int globalVar = 1;
void (^myBlock)(void) = ^{
// 1. Global variable — NOT captured, accessed directly
NSLog(@"global: %d", globalVar);
// 2. Static local — captured by POINTER (address)
static int staticLocal = 2;
staticLocal++;
NSLog(@"static: %d", staticLocal); // 3 — modified!
// 3. Auto variable — captured by VALUE (copy)
int autoLocal = 3;
// autoLocal = 4; // COMPILE ERROR: cannot assign to variable
NSLog(@"auto: %d", autoLocal);
};
myBlock();
myBlock();
// Results:
// global: 1 (unchanged — accessed directly)
// static: 3 (modified — captured by pointer, shared)
// static: 4 (modified again — static persists across calls)
// auto: 3 (copy, independent of external)
// Why auto variables can’t be captured by pointer:
// They live on the stack and are destroyed when scope ends.
// If the Block captured a pointer, it would point to garbage after
// the function returns — crash!
Code 4: __block Modifier — Stack to Heap
__block solves the “can’t modify captured auto variables” problem by wrapping the variable in a heap-allocatable structure:
// WITHOUT __block — value capture, can’t modify
int counter = 0;
void (^noBlock)(void) = ^{
counter++; // COMPILE ERROR!
};
// WITH __block — reference capture, CAN modify
__block int counter = 0;
void (^yesBlock)(void) = ^{
counter++; // OK! modifies the original
};
yesBlock();
NSLog(@"%d", counter); // 1 — original modified!
// What __block generates in clang output:
// __block int counter = 0;
// becomes:
struct __Block_byref_counter_0 {
void *isa; // always NULL for byref
__Block_byref_0 *forwarding; // points to itself (or heap copy)
volatile int32_t flags;
uint32_t size;
int counter; // the actual value lives here
};
// The __forwarding pointer is the key:
// When on stack: forwarding → itself
// When copied to heap: forwarding → heap copy
// No matter what, you always reach the real value.
// Demo: __block survives copy to heap
__block int shared = 100;
void (^stack)(void) = ^{
shared++;
NSLog(@"shared: %d", shared); // 101
};
void (^heap)(void) = [stack copy]; // copied to heap
heap(); // still works! shared: 102
NSLog(@"original: %d", shared); // 102 — same value, same memory via __forwarding
Code 5: Retain Cycle with Block
// THE PROBLEM: Retain cycle
@interface NetworkManager : NSObject
@property (nonatomic, copy) void (^onComplete)(NSData *data);
@end
@implementation NetworkManager
- (void)fetchData {
self.onComplete = ^(NSData *data) {
// self captures self — RETAIN CYCLE!
NSLog(@"Fetched %lu bytes", data.length);
[self processResult:data]; // self → onComplete block → self
};
}
- (void)processResult:(NSData *)data {
NSLog(@"Processing...");
}
- (void)dealloc {
NSLog(@"NetworkManager deallocated"); // NEVER called! Memory leak.
}
@end
// THE FIX: __weak breaks the cycle
- (void)fetchData {
__weak typeof(self) weakSelf = self;
self.onComplete = ^(NSData *data) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return; // self might be gone
NSLog(@"Fetched %lu bytes", data.length);
[strongSelf processResult:data];
};
// self → onComplete block → weakSelf (NOT self) — no cycle!
}
// Under ARC, __weak capture means:
// - The Block does NOT increase the object’s retain count
// - The weak reference automatically becomes nil when the object deallocates
// - You must check for nil before using the strongSelf
// Why __strong inside the Block?
// To keep the object alive during the Block’s execution.
// Without strongSelf, the object could be deallocated mid-execution.
Code 6: Memory Management — Stack vs Heap Block
// Stack Block — NO retain/release of captured objects
void (^stackBlock)(void); {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"retainCount before: %lu", (unsigned long)[obj retainCount]);
stackBlock = ^{
NSLog(@"captured: %p", obj);
};
// obj is on stack, stackBlock doesn’t retain it
}
// stackBlock goes out of scope here — but it’s on the stack, so it’s just abandoned.
// Heap Block — DOES retain captured objects
void (^heapBlock)(void); {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"retainCount before: %lu", (unsigned long)[obj retainCount]); // 1
heapBlock = [^{ NSLog(@"captured: %p", obj); } copy]; // copy → heap
NSLog(@"retainCount after: %lu", (unsigned long)[obj retainCount]); // 2!
}
// The heap Block retained obj (retainCount 1→2).
// When the heap Block is released, dispose_helper releases obj (retainCount 2→1).
// This is handled by copy_helper and dispose_helper in the Block descriptor:
// copy_helper: calls _Block_object_assign for each captured object
// dispose_helper: calls _Block_object_dispose for each captured object
// These are generated by the compiler for every Block that captures objects.
How to Verify with clang
// Run this command to see the C++ translation of any Objective-C Block:
clang -rewrite-objc main.m -o main.cpp
// What you’ll see in the generated main.cpp:
// 1. Block_layout structure with isa, flags, invoke, descriptor
// 2. Captured variables stored as struct members (by value or by __block ref)
// 3. copy_helper / dispose_helper functions for memory management
// 4. The __forwarding pointer inside __Block_byref structures
// You can also use Xcode’s debugger to inspect Block types at runtime:
// (lldb) p (void *)block.isa
// returns: __NSConcreteStackBlock or __NSConcreteMallocBlock or __NSConcreteGlobalBlock
// This is the single most useful command for understanding Block internals.
// I keep it in my cheat sheet because I use it during every Block-related code review.
Summary
Understanding Block internals comes down to three key insights:
• Block is an object with an isa pointer pointing to Stack/Malloc/Global class. The compiler decides the type based on usage context.
• Variable capture depends on type: global vars accessed directly, static locals captured by pointer, auto vars captured by value. __block wraps auto vars in a __Block_byref structure with a __forwarding pointer for heap-safe modification.
• Memory management is in copy/dispose helpers: stack Blocks don’t retain objects; heap Blocks do. Retain cycles happen when a heap Block retains its capturing object. Use __weak to break the chain.
Use clang -rewrite-objc to verify any Block’s underlying structure yourself. Once you see the generated C++, the “magic” of Blocks disappears and you can reason about them with confidence.

1 Comment
Easy to understand with proper difficulty, suitable for all developers.