首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> 關於C

Objective-C Block 深度解析:變量捕獲、存儲域與循環引用破解

     編輯:關於C

Block 是 Objective-C 最強大也最易誤用的特性之一。本文從編譯器視角(clang -rewrite-objc)剖析 Block 內部結構,詳細講解自動變量捕獲機制、__block 修飾符原理、三種存儲域(棧/堆/全局)轉換規則、ARC/非ARC 差異、循環引用解決方案,並附帶五道經典面試題深度解析。

一、Block 本質:偽裝成對象的結構體

源碼:

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 和捕獲變量副本,可視為「匿名函數對象」。

二、自動變量捕獲:值傳遞與 const 化

int val = 10;
void (^blk)(void) = ^{ printf("val=%d\n", val); };
val = 2;
blk();  // 輸出 10,而非 2
  • 捕獲瞬間的值副本,存入 Block 結構體字段
  • Block 內部訪問變為 __cself->val,原變量修改不影響 Block
  • 嘗試在 Block 內修改捕獲變量 → 編譯錯誤(隱式 const)

三、__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 持有 __block_byref_val_0* 指針
  • __forwarding 始終指向當前有效實例(棧→堆遷移時更新)
  • 訪問變為 __cself->val->__forwarding->val,實現跨存儲域統一訪問

四、三大存儲域與 ARC/非ARC 行為差異

場景非 ARCARC
無捕獲 / 只捕獲全局/靜態全局 (_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 環境)

  • A:立即調用,棧幀未銷毀,安全
  • B:Block 進入數組延長生命週期,非 ARC 無自動 copy → 崩潰
  • C:無捕獲 → 全局 Block,永遠安全
  • D/E:返回棧上 Block,調用時棧幀已銷毀 → 野指針崩潰

六、循環引用與解決方案

成因:self → Block → self(Block 持有捕獲對象 strong 引用)

方案 1:__weak 弱引用(推薦)

__weak typeof(self) weakSelf = self;
void (^blk)(void) = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) { /* 使用 strongSelf */ }
};

方案 2:__block + 手動置 nil(舊代碼兼容)

__block typeof(self) tmp = self;
void (^blk)(void) = ^{
    /* 使用 tmp */
    tmp = nil;  // 打破循環,必須執行到此處
};

缺點:依賴 Block 執行路徑,異常/提前返回會洩漏;不推薦新代碼使用。

七、__block 變量與對象的內存管理

  • Block copy 時:__block 變量隨 Block 遷移到堆,__forwarding 更新指向堆副本
  • 捕獲 Objective-C 對象:編譯器生成 _Block_object_assign (retain) 和 _Block_object_dispose (release) 調用
  • 標誌位區分:BLOCK_FIELD_IS_BYREF (__block 變量) vs BLOCK_FIELD_IS_OBJECT (普通對象)

總結

  1. Block 本質是捕獲上下文的函數對象,內存佈局由編譯器自動合成
  2. 非 ARC 必須手動 copy 棧上 Block 到堆,ARC 自動處理但需理解時機
  3. __block 通過間接層 (__forwarding) 實現跨域共享可變狀態
  4. 循環引用首選 __weak 方案,配合 strongSelf 內部強引用保證執行期存活
  5. 調試時善用 clang -rewrite-objc 觀察結構體佈局,理解捕獲語義
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved