文章目錄
- 前言
- Block的聲明和創建
- 問題引入
- Block的底層結構
- Block的執行流程
- Block的創建與存儲
- Block的傳遞與調用
- Block的捕獲機制
- 捕獲局部變量
- 捕獲全局變量
- 小結
- Block的類型
- __block修飾符
- __block變量的包裝結構體
- block的實例結構體
- block的執行邏輯
- Block循環引用
- 造成的原因
- 解決方法
- 小結
- 總結
前言
??最近在復習OC知識,發現自己對于block的了解很少,很多東西當時都沒有搞明白或者甚至不知道,然后就對block進行了重新學習。
Block的聲明和創建
Block 的語法格式為:
返回值類型 (^Block名稱)(參數列表) = ^返回值類型(參數列表) { 代碼塊 };
示例:
// 聲明并創建一個無參數、無返回值的 Block
void (^myBlock)(void) = ^void(void) {NSLog(@"Block 執行");
};// 調用 Block
myBlock(); // 輸出:"Block 執行"
問題引入
由此,我們有以下代碼,后面所有的實例分析都是在此基礎上:
運行結果:
我們可以看到,上述代碼進行了兩次block捕獲,第一次捕獲A和B的初始值,A為3,B為7;然后在myblock函數外對A、B變量進行自增修改處理,再次調用myblock函數進行捕獲,變量A輸出不變為3,變量B輸出為自增后的值8,控制變量可以發現,造成這種結果,唯一不同的是我們在定義變量B時,在前面用了==__block==進行修飾。
由此我們產生了 一系列問題:block內部是什么結構?為什么在block外部修改變量不會影響到其在block內的捕獲?為什么聲明時用__block進行修飾后,外部修改變量block捕獲也會變?可不可以直接在block內部進行變量修改?
下面,我們來對這些問題進行一一探討。
Block的底層結構
我們用clang -rewrite-objc main.m -o main.cpp
將上述代碼文件轉換成C++文件:
int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int A = 3;__attribute__((__blocks__(byref))) __Block_byref_B_0 B = {(void*)0,(__Block_byref_B_0 *)&B, 0, sizeof(__Block_byref_B_0), 7};void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, A, (__Block_byref_B_0 *)&B, 570425344));((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);A++;(B.__forwarding->B)++;z((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);}return 0;
}
我們可以看到,我們定義myblock函數的代碼在C++中是這樣的:
上述代碼Block底層實現的編譯器生成代碼,代碼的核心是將一個 Block 的底層結構體指針轉換為函數指針,并賦值給 myblock變量,具體形式:
void(*myblock)(void) = ((void (*)())&__main_block_impl_0(...));
等號左邊等名義個函數指針myblock,類型為void(*)(void)
(無參數、無返回值);
等號右邊通過調用__main_block_impl_0
函數生成 Block 的底層結構體,并將其轉換為函數指針后賦值給 myblock。
__main_block_impl_0
是編譯器自動生成的 Block 初始化函數,負責創建 Block 的底層結構體(如 struct __block_impl
)并初始化其核心字段。其參數通常包括:
參數 | 類型/含義 |
---|---|
(void *)__main_block_func_0 | 指向 Block 實際執行邏輯的函數指針(即 FuncPtr字段的內容) |
&__main_block_desc_0_DATA | 指向 Block 描述符(struct __block_descriptor)的指針,包含 Block 的元數據(如大小、復制/銷毀函數) |
A | 被 Block 捕獲的變量(如基本類型、不可變對象) |
(__Block_byref_B_0 *)&B | 指向 __Block_byref_B_0結構體的指針,用于捕獲可變對象或需要引用捕獲的變量 |
570425344 | 標志位(可能表示 Block 的行為選項,如是否復制捕獲變量、是否啟用優化等) |
從剛剛我們在main.m的cpp文件里看到的定義函數,我們繼續探究找尋__main_block_impl_0函數的源碼定義:
struct __main_block_impl_0
包含以下核心成員:
成員 | 類型/含義 |
---|---|
impl | struct __block_impl類型,Block 的核心實現結構體(封裝函數指針、類型標識、標志位等)。 |
Desc | struct __main_block_desc_0*類型,指向 Block 描述符(存儲元數據,如復制/銷毀函數)。 |
A | int類型,捕獲的整型變量(值捕獲)。 |
B | __Block_byref_B_0*類型,指向引用捕獲的可變對象的輔助結構體(引用捕獲)。 |
構造函數 | 初始化各成員,綁定 Block 的執行邏輯、捕獲變量和元數據。 |
然后我們順藤摸瓜找到了Block 的核心實現結構體struct __block_impl函數的源碼:
由上述過程,我們可以知道,Block的底層本質是一個結構體,內部封裝了一些關鍵信息:
函數指針:指向Block對應的可執行代碼
捕獲的變量:Block執行時需要訪問的外部變量()
執行上下文:包括 Block 的引用計數、所屬的類(用于調試)等元數據
有一張圖非常好地說明了block的底層調用和邏輯:
? ——圖片來自博客【iOS】Block底層分析@zhngxvy
Block的執行流程
Block 的執行分為??創建??、??存儲??、??傳遞??和??調用??四個階段,核心是??函數指針的調用??和??捕獲變量的管理??。
Block的創建與存儲
創建:通過 ^
語法定義 Block 時,編譯器會生成一個 struct Block_layout
結構體實例,并將代碼塊編譯為對應的機器指令(存儲在invoke函數指針中)。
存儲位置:Block 初始存儲在棧上(棧 Block),但以下場景會觸發 Block 被復制到堆上(堆 Block):
- Block 被賦值給一個強引用的變量(如
__strong
修飾的屬性或局部變量)。 - Block 被作為參數傳遞給一個異步函數(如
dispatch_async
)。 - Block 被顯式復制(調用
Block_copy()
)。
Block的傳遞與調用
Block 可以像對象一樣被傳遞(如作為方法參數、存儲在集合中),其調用的本質是執行 invoke函數指針,并傳遞參數。
示例(Block 作為方法參數):
// 定義一個接受 Block 的方法
- (void)executeBlock:(void (^)(void))block {NSLog(@"準備執行 Block...");block(); // 調用 Block(執行 invoke 函數)NSLog(@"Block 執行完成");
}// 使用
[self executeBlock:^{NSLog(@"自定義 Block 邏輯");
}];
執行流程:
- 定義 Block 時,編譯器生成棧上的
struct Block_layout
。 - 將 Block 作為參數傳遞給
executeBlock:
方法時,Block 被復制到堆上(因方法參數需要強引用)。 - 方法內部調用
block()
時,觸發invoke函數指針,執行 Block 的代碼邏輯。
Block的捕獲機制
Block 可以捕獲外部作用域的變量(如局部變量、實例變量),捕獲行為分為兩種:值捕獲和指針捕獲。
捕獲局部變量
auto:自動變量,離開作用域就自動銷毀,只存在于局部變量(沒有特別關鍵字修飾,一般默認為auto)
static:靜態局部變量
我們先來看以下代碼:
#import <Foundation/Foundation.h>int main(int argc, const char * argv[]) {@autoreleasepool {int A = 18;//局部變量(自動變量)static int B = 52;//靜態局部變量void(^jubuBlock)(void) = ^{NSLog(@"jubuBlock - A:%d - B:%d", A, B);};jubuBlock();NSLog(@"輸出1 - A:%d B:%d", A, B);A = 20;B = 100;jubuBlock();NSLog(@"輸出2 - A:%d B:%d", A, B);}return 0;
}
輸出結果如下:
由此,我們可以發現,當我們在block函數外部修改局部變量時,block函數內部的自動變量不會受影響,而靜態局部變量會跟著被修改。這是為什么呢?
為了探尋這一原因,我們將上述代碼文件轉化為cpp文件,我們可以發現__main_block_impl_0
結構體定義如下:
我們可以得到以下結論:
- A:捕獲的自動變量(按值復制)。
- B:捕獲的靜態局部變量(按指針復制,存儲其內存地址)。
這就可以解釋剛剛的問題:為什么在block函數外部修改自動變量,內部的變量值不會產生改變,而修改靜態變量就會變?到這里,block捕獲局部變量的本質就很明顯了:
自動變量:按值捕獲(復制其當前值到block內部)。即使外部變量后續被修改,block內部使用的仍是捕獲時的副本。
靜態局部變量:按指針捕獲(存儲其內存地址)。外部變量修改時,block通過指針訪問的是最新值。
捕獲全局變量
首先,有以下代碼:
#import <Foundation/Foundation.h>int A = 18;//全局變量
static int B = 52;//靜態全局變量int main(int argc, const char * argv[]) {@autoreleasepool {void(^quanjuBlock)(void) = ^{NSLog(@"quanjuBlock - A:%d - B:%d", A, B);};quanjuBlock();NSLog(@"輸出1 - A:%d B:%d", A, B);A = 20;B = 100;quanjuBlock();NSLog(@"輸出2 - A:%d B:%d", A, B);}return 0;
}
輸出結果:
我們再來看這部分源碼:
我們發現,這部分的源碼跟上面局部變量有點出入,在全局變量一直在__main_block_impl_0
結構體定義中并沒有我們聲明的全局變量A和B,即全局、全局靜態變量并沒有出現在我們的Block實現結構體中,說明二者無法被捕獲。而全局變量存儲在內存中,打印的一直是最新的值。
小結
自動變量(局部變量) | 靜態局部變量 | 全局變量 | |
---|---|---|---|
存儲區域 | 棧(Stack) | 全局數據區(Data Segment) | 全局數據區(Data Segment) |
生命周期 | 隨作用域結束(如函數返回)銷毀 | 程序啟動時創建,程序結束時銷毀 | 程序啟動時創建,程序結束時銷毀 |
默認捕獲機制 | 按值拷貝(復制當前值到block內部) | 按指針拷貝(存儲變量內存地址) | 按指針拷貝(存儲變量內存地址) |
block內訪問形式 | 直接使用拷貝后的副本(獨立于原變量) | 通過指針解引用訪問原變量 | 通過指針解引用訪問原變量 |
外部修改的影響 | 不影響block內部的副本(block捕獲的是歷史值) | 影響block內部(指針指向的原變量被修改) | 影響block內部(指針指向的原變量被修改) |
是否支持修改捕獲值 | 默認不可直接修改(需__block 修飾) | 可直接修改(通過指針操作原變量) | 可直接修改(通過指針操作原變量) |
產生這種差異的原因:
auto和static:因為作用域的問題,自動變量的內存隨時可能被銷毀,所以要捕獲就趕緊把它的值拿進來,防止調用的時候訪問不到。
靜態局部變量:存儲在全局數據區,生命周期長于block,因此block捕獲其指針后,即使原變量所在作用域銷毀(如函數返回),仍可通過指針訪問最新值。
注意:盡管靜態局部變量的生命周期很長,但其**作用域(可訪問范圍)**僅限于聲明它的函數內部:
- 函數外部無法直接訪問該變量(即使通過指針或引用)。
- 不同函數中聲明的同名靜態局部變量相互獨立(因為各自存儲在全局數據區的不同位置)。
全局變量:在Block中訪問局部變量相當于是跨函數訪問,要先將變量存儲在Block里(捕獲),使用的時候再從Block中取出,而全局變量是直接訪問。
tips:
自動變量按值捕獲是安全的(避免懸垂指針),但可能增加內存拷貝開銷(大對象需注意)。
靜態/全局變量按指針捕獲更高效(無拷貝),但需注意多線程并發修改時的線程安全問題。
Block的類型
上述說到Block含有isa指針,而OC對象的isa指針指向它的類型,那么Block的類型都有哪些呢?
首先有以下代碼:
void (^block)(void) = ^{NSLog(@"hello world!");
};
NSLog(@"%@ %@", block, [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
運行后結果如下:
我們可以發現這個block的類型是NSGlobalBlock,其父類是NSBlock,根父類是NSObject,這也能說明Block是一個OC對象。
除了NSGlobalBlock,Block還有哪些類型,我們先來看如下代碼:
void (^block1)(void) = ^{NSLog(@"hello world!");};NSLog(@"%@ %@", block1, [block1 class]);int A = 3;void (^block2)(void) = ^{NSLog(@"%d", A);};NSLog(@"%@ %@", block2, [block2 class]);NSString *string = @"hello world!";void (^block3)(void) = ^{NSString *stringCopy = [string copy];};NSLog(@"%@ %@", block3, [block3 class]);
當我們在ARC環境下運行代碼后,有如下結果:
隨后,我們在項目的Bulid Settings中關閉ARC:
隨即得到如下結果:
我們可以發現,在MRC環境下,若block不捕獲任何外部變量,且未被復制,則其類型為__NSGlobalBlock__
;若block捕獲外部變量,但未被復制,則其類型為__NSStackBlock__
;若block被復制,則其類型為__NSMallocBlock__
。
但如果是在ARC環境下,即使不顯式調用copy也會是__NSMallocBlock__
類型,這是因為在ARC下,編譯器會自動插入內存管理代碼(如retain
、release
、copy
),核心目的是確保block在需要跨作用域存活時保持有效,確保對象(包括block)在其生命周期內被正確管理。
所以,我們可以做出如下總結:
存儲位置 | 觸發條件 | 生命周期 |
---|---|---|
棧上 | block未被復制(僅在局部作用域使用,未被賦值給id類型變量或作為參數傳遞) | 離開作用域時自動銷毀(無法跨作用域使用) |
堆上 | block被復制(顯式調用copy,或隱式被id類型變量持有、作為方法參數傳遞等)。 | 由引用計數管理,直到release計數歸零后銷毀。 |
全局區 | block不捕獲任何外部變量,且未被復制。 | 程序啟動到結束時一直存在(全局唯一)。 |
__block修飾符
在我們前面的學習中,我們知道在block函數外部修改自動變量,函數內部捕獲的值不會受到影響,那如果我們想修改在block中捕獲的自動了變量的值怎么辦?
首先,我們肯定想到說,想修改?那我直接在函數內部進行修改行不行!我們試一下:
我們會發現,在block函數內部直接修改自動變量會發生報錯:Variable is not assignable (missing __block type specifier)
,翻譯過來就是:變量未分配(丟失 __block 類型限定符)。
這是因為自動變量的默認捕獲機制:在OC中,block對自動變量的默認捕獲方式是按值復制Copy by Value):
- 當block定義時,會復制自動變量的當前值到block內部的副本中。
- block內部訪問該變量時,實際訪問的是副本,而非原變量。
- 因此,若在block內部嘗試修改該變量(如賦值操作),編譯器會報錯:因為修改的是副本,原變量無法被block直接修改,這是不允許的。
那么我們真的沒有辦法修改自動變量了嗎😭有的兄弟有的!
如果我們需要在block內部修改自動變量,需用__block
修飾符聲明該變量。好的,我們現在進行實際操作試試😋:
OK!對自由變量A加了__block修飾后就可以在block函數內部進行修改了?
嘶~這個__block到底有什么魔力?下面我們來探索一下。
先說結論:__block
的作用是將自動變量轉換為塊級變量(Block Variable),使其被block和原作用域共享同一實例。
__block變量的包裝結構體
現在我們來看看__block這部分的cpp源碼:
我們發現,被__block修飾的自動變量A在源碼中,編譯器生成了一個包裝結構體__Block_byref_A_0
,用于存儲被修飾變量的值及其元數據。
其成員含義:
成員 | 類型 | 說明 |
---|---|---|
__isa | void* | 指向類對象的指針(通常為_NSConcreteStackBlock或堆上的類,初始可能為NULL)。 |
__forwarding | __Block_byref_A_0* | 轉發指針,指向變量的實際存儲位置(棧或堆)。當block被拷貝到堆時,此指針會指向堆中的副本。 |
__flags | int | 標志位(如是否被拷貝、是否需要釋放等)。 |
__size | int | 結構體的大小(用于內存管理)。 |
A | int | 被__block修飾的原始變量 |
通過此結構體,block和原作用域的變量共享同一內存空間,實現“跨作用域修改變量”的能力。
block的實例結構體
block的實例結構體( __main_block_impl_0)源碼如下:
block的實例通過此結構體表示,包含block的執行邏輯、描述信息及對__block
變量的引用:
結構體成員 | 類型 | 說明 |
---|---|---|
impl | struct __block_impl | block的基類結構體,包含isa(類型標識)、Flags(標志位)、FuncPtr(執行函數指針) |
Desc | struct __main_block_desc_0* | 指向block描述符的指針,記錄block的內存布局和回調函數(如拷貝、銷毀) |
A | __Block_byref_A_0* | 指向__block變量包裝結構體的指針(通過引用捕獲,而非值拷貝) |
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_A_0 *_A, int flags=0) : A(_A->__forwarding) { // 初始化時,A指向包裝結構體的轉發指針impl.isa = &_NSConcreteStackBlock; // 標記為棧上blockimpl.Flags = flags; // 設置標志位(如是否需要拷貝)impl.FuncPtr = fp; // 綁定執行函數(如__main_block_func_0)Desc = desc; // 綁定描述符
}
A(_A->__forwarding)表示block實例的A成員直接指向__Block_byref_A_0
結構體的__forwarding
指針。這確保了無論block是否被拷貝到堆,A始終指向變量的實際存儲位置(棧或堆)。
block的執行邏輯
block的實際執行體(__main_block_func_0:),其源碼如下:
通過__cself(block實例自身)訪問捕獲的變量。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_A_0 *A = __cself->A; // 獲取包裝結構體指針(A->__forwarding->A) = 19; // 修改被包裝的變量A的值(通過轉發指針)NSLog((NSString *)&__NSConstantStringImpl__... , (A->__forwarding->A)); // 打印修改后的值
}
通過A->__forwarding->A
訪問被__block修飾的變量A。__forwarding
指針確保即使block被拷貝到堆,仍能正確訪問變量的實際存儲位置(棧或堆副本)。
Block循環引用
Block的本質是一個對象(繼承自NSObject),其內存管理遵循ARC規則。當Block捕獲外部對象(如self)時:
-
默認情況下,Block會強引用捕獲的對象(對對象類型變量按引用捕獲,基本類型按值捕獲)。
-
如果外部對象(如self)本身強引用該Block(例如將Block作為self的屬性),會形成強引用閉環:
self → Block(強引用) → self(強引用)
此時兩者的引用計數均無法歸零,導致內存泄漏。
造成的原因
Block作為對象的屬性,且內部捕獲self
當對象的屬性是Block,且Block內部訪問了self
(如調用self
的方法或屬性),同時對象強引用該Block時,形成循環引用。
// ViewController.h
@interface ViewController : UIViewController
@property (nonatomic, strong) void (^myBlock)(void); // Block屬性(強引用)
@end// ViewController.m
@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// 1. 創建Block,內部捕獲self(強引用)self.myBlock = ^{ NSLog(@"Self: %@", self); // 捕獲self(強引用)};// 2. self強引用myBlock(屬性是strong)// 此時形成循環引用:self → myBlock → self
}@end
循環路徑:
self(ViewController實例)通過strong屬性強引用myBlock→ myBlock通過默認的強引用捕獲self→ 兩者引用計數均無法歸零。
Block被嵌套對象持有,且捕獲外層self
當Block被另一個對象(如manager)持有,而self又持有該manager,同時Block內部捕獲self時,也可能形成循環。
// Manager.h
@interface Manager : NSObject
@property (nonatomic, copy) void (^block)(void);
@end// ViewController.m
@implementation ViewController {Manager *_manager;
}- (void)viewDidLoad {[super viewDidLoad];_manager = [[Manager alloc] init];// 1. Manager持有block(copy后為強引用)_manager.block = ^{ NSLog(@"ViewController: %@", self); // Block捕獲self(強引用)};// 2. self持有_manager(強引用)// 循環路徑:self → _manager → block → self
}@end
解決方法
??解決循環引用的核心是打破強引用閉環,通常通過==弱引用(__weak
或__unsafe_unretained
)==弱化Block對self的引用
使用__weak修飾self
在Block內部通過__weak
修飾的weakSelf引用self,避免Block強引用self。
// ViewController.m
@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// 1. 定義弱引用指向self(打破循環)__weak typeof(self) weakSelf = self;// 2. Block捕獲weakSelf(弱引用)self.myBlock = ^{ // 使用weakSelf替代self(可能為nil)NSLog(@"Self: %@", weakSelf); };// 3. self仍強引用myBlock,但myBlock弱引用self → 無閉環
}@end
__weak typeof(self) weakSelf = self
創建了一個弱引用weakSelf,其引用計數不增加,Block捕獲weakSelf(弱引用),因此self的引用計數不會因Block的持有而增加。
循環路徑被打破:self → myBlock → weakSelf(弱引用,不增加計數)。
使用__unsafe_unretained
(不推薦)
__unsafe_unretained
與__weak
類似,但不會自動將失效的指針置為nil,可能導致野指針(訪問已釋放的對象)。
__unsafe_unretained typeof(self) unsafeSelf = self;
self.myBlock = ^{ NSLog(@"Self: %@", unsafeSelf); // 若self已釋放,unsafeSelf可能為野指針
};
__unsafe_unretained
僅在以下場景使用:目標對象生命周期明確短于Block(無需置nil)或無法使用__weak
(如兼容舊版本iOS)。
在Block執行完畢前確保self存活
若Block需要長時間執行(如異步任務),可通過臨時強引用確保self在Block執行期間不被釋放,執行完畢后自動釋放。
__weak typeof(self) weakSelf = self;
self.myBlock = ^{__strong typeof(weakSelf) strongSelf = weakSelf; // 臨時強引用if (strongSelf) {[strongSelf doSomething]; // 在Block執行期間,strongSelf保持self存活}
}; // strongSelf在此處銷毀,不影響self的引用計數
__strong typeof(weakSelf) strongSelf = weakSelf
在Block內部創建一個臨時強引用strongSelf,若weakSelf未失效(self仍存活),strongSelf會強引用self,確保Block執行期間self不被釋放,Block執行完畢后,strongSelf銷毀,self的引用計數恢復正常。
小結
所以,我們可以總結:Block循環引用的核心是強引用閉環,解決方案的關鍵是弱化其中一個環節的引用。最常用的方法是使用__weak修飾self,在Block內部通過弱引用訪問self,打破循環。同時需注意:
- 避免在Block內部直接強引用self(默認行為)。
- 若需在Block執行期間確保self存活,可結合臨時強引用(__strong)。
__unsafe_unretained
需謹慎使用,防止野指針崩潰。
總結
??Block是Objective-C中強大的閉包工具,在iOS開發中十分重要,常用于處理異步操作、回調、集合操作等,核心能力是捕獲并保存環境狀態,支持靈活的異步編程和回調邏輯。理解其存儲位置(棧/堆/全局)、變量捕獲規則(值/指針/引用)、內存管理(ARC/MRC)及循環引用解決方案是掌握Block的關鍵。