Block 是 Objective-C 最強大也最易誤用的特性之一。本文從編譯器視角(clang -rewrite-objc)剖析 Block 內部結構,詳細講解自動變量捕獲機制、__block 修飾符原理、三種存儲域(棧/堆/全局)轉換規則、ARC/非ARC 差異、循環引用解決方案,並附帶五道經典面試題深度解析。
源碼:
int main() {
void (^blk)(void) = ^{ printf("block\n"); };
blk();
return 0;
}
clang -rewrite-objc 產出核心結構:
struct __block_impl {
void *isa; // 對象標識
int Flags; // 存儲域標誌
int Reserved;
void *FuncPtr; // 執行函數指針
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc; // 描述信息(大小、捕獲變量布局)
// 捕獲的變量作為字段內嵌
int val; // 例:捕獲 int val
};
結論:Block 實質是一個帶有 isa 指針的結構體,擁有執行入口 FuncPtr 和捕獲變量副本,可視為「匿名函數對象」。
int val = 10;
void (^blk)(void) = ^{ printf("val=%d\n", val); };
val = 2;
blk(); // 輸出 10,而非 2
__cself->val,原變量修改不影響 Blockconst)__block 修飾符:打破值傳遞限制__block int val = 10;
void (^blk)(void) = ^{ val = 1; }; // 合法
編譯器將 val 包裝為堆/棧雙向鏈表節點:
struct __block_byref_val_0 {
void *__isa;
struct __block_byref_val_0 *__forwarding; // 指向自身(棧)或堆副本
int _flags;
int __size;
int val; // 真實數據
};
__block_byref_val_0* 指針__forwarding 始終指向當前有效實例(棧→堆遷移時更新)__cself->val->__forwarding->val,實現跨存儲域統一訪問| 場景 | 非 ARC | ARC |
|---|---|---|
| 無捕獲 / 只捕獲全局/靜態 | 全局 (_NSConcreteGlobalBlock) | 全局 |
| 捕獲自動變量 | 棧 (_NSConcreteStackBlock) | 堆 (_NSConcreteMallocBlock) |
棧上 Block copy | → 堆,引用計數+1 | 無操作(已在堆) |
堆上 Block copy | 引用計數+1 | 引用計數+1 |
全局 Block copy | 無操作 | 無操作 |
檢測技巧:對比 Block 內外 __block 變量地址是否一致(非 ARC 棧上不一致,ARC/複製後一致)。
// 題1:棧上 Block 逃逸
void exampleA() {
char a = 'A';
^{ printf("%c\n", a); }(); // 立即調用,安全
}
// 題2:Block 存入數組後逃逸
void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{ printf("%c\n", b); }]; // 非ARC:棧Block入數組→野指針
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^blk)() = [array objectAtIndex:0];
blk(); // 非ARC崩潰,ARC安全(自動copy)
}
// 題3:無捕獲自動變量
void exampleC_addBlockToArray(NSMutableArray *array) {
[array addObject:^{ printf("C\n"); }]; // 全局Block,安全
}
// 題4:函數返回棧上Block
typedef void (^dBlock)();
dBlock exampleD_getBlock() {
char d = 'D';
return ^{ printf("%c\n", d); }; // 非ARC返回棧Block→野指針
}
// 題5:中間變量持有
typedef void (^eBlock)();
eBlock exampleE_getBlock() {
char e = 'E';
void (^blk)() = ^{ printf("%c\n", e); };
return blk; // 同題4,非ARC崩潰
}
答案:A B A B B(非 ARC 環境)
成因:self → Block → self(Block 持有捕獲對象 strong 引用)
__weak 弱引用(推薦)__weak typeof(self) weakSelf = self;
void (^blk)(void) = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) { /* 使用 strongSelf */ }
};
__block + 手動置 nil(舊代碼兼容)__block typeof(self) tmp = self;
void (^blk)(void) = ^{
/* 使用 tmp */
tmp = nil; // 打破循環,必須執行到此處
};
缺點:依賴 Block 執行路徑,異常/提前返回會洩漏;不推薦新代碼使用。
__block 變量與對象的內存管理copy 時:__block 變量隨 Block 遷移到堆,__forwarding 更新指向堆副本_Block_object_assign (retain) 和 _Block_object_dispose (release) 調用BLOCK_FIELD_IS_BYREF (__block 變量) vs BLOCK_FIELD_IS_OBJECT (普通對象)copy 棧上 Block 到堆,ARC 自動處理但需理解時機__block 通過間接層 (__forwarding) 實現跨域共享可變狀態__weak 方案,配合 strongSelf 內部強引用保證執行期存活clang -rewrite-objc 觀察結構體佈局,理解捕獲語義