本節(jié)將深入探索block的底層原理。
- 循環(huán)引用 & 解決方案
- block結(jié)構(gòu)分析
- 源碼探索
- block的參數(shù)處理(多次拷貝)
1. 循環(huán)引用 & 解決方案
-
正常釋放:
image.png -
循環(huán)引用:
image.png
- 解決循環(huán)引用的方法:
weakSelf弱引用self,搭配strongSelf- 使用
__block修飾對(duì)象(必須在block中置空對(duì)象,且block必須被調(diào)用)傳對(duì)象self作參數(shù),提供給代碼塊使用
- 測試代碼:
typedef void(^HTBlock)(void);
typedef void(^HTBlock2)(ViewController *);
@interface ViewController ()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) HTBlock block;
@property (nonatomic, copy) HTBlock2 block2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.name = @"ht";
// // 循環(huán)引用 [self持有block,block持有self]
// self.block = ^(void){
// NSLog(@"%@",self.name);
// };
// self.block();
//
// // 沒有循環(huán)引用 (UIView的block持有self,self與UIView的block無關(guān))
// [UIView animateWithDuration:1 animations:^{
// NSLog(@"%@",self.name);
// }];
// 方法1: `weakSelf`弱引用`self`,搭配`strongSelf`
// weak不會(huì)讓self的引用計(jì)數(shù)+1,所以不影響self的釋放。 而strongSelf持有的是weakSelf。
// 當(dāng)self釋放時(shí),如果block執(zhí)行完了,strongSelf局部變量就會(huì)被釋放,此時(shí)weakSelf也被釋放。所以不會(huì)造成循環(huán)引用
// __weak typeof(self) weakSelf = self;
// self.block = ^{
// __strong typeof(self) strongSelf = weakSelf;
// NSLog(@"%@",strongSelf.name);
// };
// self.block();
// // 方法2:使用`__block`修飾對(duì)象(`必須`在block中`置空對(duì)象`,且block必須`被調(diào)用`)
// __block ViewController * vc = self;
// self.block = ^(void){
// NSLog(@"%@",vc.name);
// vc = nil; // 必須手動(dòng)釋放。 因?yàn)関c持有了self,vc不釋放,self永遠(yuǎn)不可能釋放。
// };
// self.block(); // 必須調(diào)用block。 因?yàn)関c持有了self,不過不執(zhí)行block,vc永遠(yuǎn)沒有nil,self永遠(yuǎn)不會(huì)釋放
// 方法3:`傳`對(duì)象`self`作`參數(shù)`,提供`給代碼塊使用`
// 最佳的使用方式,因?yàn)椴粫?huì)影響self的正常釋放。(調(diào)用時(shí),引用計(jì)數(shù)會(huì)+1,但是調(diào)用完后,就會(huì)-1,不影響self的生命周期)
self.block2 = ^(ViewController * vc) {
NSLog(@"%@",vc.name);
};
self.block2(self);
}
@end
2. block結(jié)構(gòu)分析
2.1 block的結(jié)構(gòu)
-
main.m文件中添加測試代碼:
#import <stdio.h>
int main(int argc, const char * argv[]) {
void(^block)(void) = ^{
printf("HT");
};
block();
return 0;
}
-clang編譯main.m文件:
clang -rewrite-objc main.m -o main.cpp
- 打開
main.cpp文件:
image.png - 可以發(fā)現(xiàn)
block實(shí)際上是一個(gè)結(jié)構(gòu)體,所以block支持%@打印。
- 函數(shù)聲明(創(chuàng)建):
- block是個(gè)
結(jié)構(gòu)體,初始化時(shí),存儲(chǔ)執(zhí)行代碼塊(匿名函數(shù))和基礎(chǔ)描述信息。
- 函數(shù)調(diào)用:
- 調(diào)用了
FuncPtr,實(shí)際就是__main_block_func_0函數(shù),入?yún)?/code>是block自己(這是為了捕獲變量)
2.2 block入?yún)⒎治?/h4>
2.2.1 直接加入變量(值拷貝)
- 加入
變量int a,重新編譯:
image.png
- 發(fā)現(xiàn)
編譯時(shí),就生成了對(duì)應(yīng)的變量。__main_block_func_0匿名函數(shù)多了一個(gè)局部變量a,這個(gè)a是讀取了cself的內(nèi)存值,是值拷貝。是只讀變量。
為了證明是a是值拷貝,只讀變量,我們?cè)?code>測試代碼的block中添加a++的賦值代碼,編譯器立馬報(bào)錯(cuò)提示:(造成了代碼歧義)
image.png
2.2.2 __block聲明變量(指針拷貝)
變量int a,重新編譯:
編譯時(shí),就生成了對(duì)應(yīng)的變量。__main_block_func_0匿名函數(shù)多了一個(gè)局部變量a,這個(gè)a是讀取了cself的內(nèi)存值,是值拷貝。是只讀變量。為了證明是a是值拷貝,只讀變量,我們?cè)?code>測試代碼的block中添加a++的賦值代碼,編譯器立馬報(bào)錯(cuò)提示:(造成了代碼歧義)

__block聲明變量(指針拷貝)將int a使用__block聲明:

與上面
直接使用int a不同:
上面?zhèn)魅?/code>的是a的值,執(zhí)行block函數(shù)時(shí),是進(jìn)行值拷貝,只讀。__block修飾后,會(huì)生成a的結(jié)構(gòu)體對(duì)象,傳入的是對(duì)象指針地址,執(zhí)行block函數(shù)時(shí),是進(jìn)行指針拷貝,可讀可寫,通過修改指針指向的內(nèi)容。完成block的內(nèi)外通訊。
3. 源碼探索
3.1 Block的三種類型
Block有【3種類型】:
-
__NSGlobalBlock__:無入?yún)?/code>時(shí),是全局Block -
__NSMallocBlock__:有外部變量時(shí),變成堆區(qū)Block -
__NSStackBlock__:有外部變量,使用__weak修飾時(shí),變成棧區(qū)Block
- 測試代碼:
- (void)demo {
// 【3種block類型】
// 1. __NSGlobalBlock__ (無入?yún)r(shí),是全局Block)
void(^block1)(void) = ^{
NSLog(@"HT_Block");
};
NSLog(@"%@",block1); // 打?。?<__NSGlobalBlock__: 0x102239040>
// 2. __NSMallocBlock__ (有外部變量時(shí),變成堆區(qū)Block)
int a = 10;
void(^block2)(void) = ^{
NSLog(@"HT_Block %d",a);
};
NSLog(@"%@",block2); // 打?。?<__NSMallocBlock__: 0x600000f970c0>
//3. __NSStackBlock__ (有外部變量,使用__weak修飾,變成棧區(qū)Block)
int b = 20;
void(^__weak block3)(void) = ^{
NSLog(@"HT_Block %d",b);
};
NSLog(@"%@",block3); // 打印 <__NSStackBlock__: 0x7ffeedae8240>
}

- 為弄清楚
底層原理,首先得確定block源碼在哪個(gè)庫中:
在
block創(chuàng)建前,加入斷點(diǎn):image.png
- 打開
匯編模式:image.png
運(yùn)行
代碼,加入objc_retainBlock符號(hào)斷點(diǎn):
image.png繼續(xù)運(yùn)行,加入
_Block_copy符號(hào)斷點(diǎn):
image.png發(fā)現(xiàn)
Block的相關(guān)操作,是在libsystem_blocks.dylib庫中:image.png
?? 源碼地址 ,搜索
libclosure-74,點(diǎn)擊右邊下載按鈕。
- 查看源碼,可以關(guān)注到
block的結(jié)構(gòu)類型為Block_layout:(后面會(huì)明白為什么是Block_layout)
image.png
Flag標(biāo)識(shí)
image.png
- 第1 位,
釋放標(biāo)記,一般常用BLOCK_NEEDS_FREE做& 位與操作,一同傳入Flags,告知該block可釋放。- 第2-16位,存儲(chǔ)
引用計(jì)數(shù)的值;是一個(gè)可選用參數(shù) (0xfffe二進(jìn)制為1111 1111 1111 1110)- 第25位,
低16位是否有效的標(biāo)志,程序根據(jù)它來決定是否增加或是減少引用計(jì)數(shù)位的值;- 第26位,是否擁有
拷貝輔助函數(shù)(是否調(diào)用_Block_call_copy_helper函數(shù)); 決定是否有block_description_2- 第27位,是否擁有
block 析構(gòu)函數(shù);- 第28位,標(biāo)志是否有
垃圾回收; //OS X- 第29位,標(biāo)志是否是
全局block;- 第30位,與
BLOCK_HAS_SIGNATURE 相對(duì),判斷是否當(dāng)前block擁有一個(gè)簽名。用于runtime時(shí)動(dòng)態(tài)調(diào)用- 第31位,是否有
簽名- 第32位,標(biāo)志是否有
Layout,使用有拓展,決定block_description_3
- Block基礎(chǔ)結(jié)構(gòu)梳理圖:

- 下面,我們通過
案例來分析和驗(yàn)證上面結(jié)構(gòu)圖
3.2 Block的類型轉(zhuǎn)變
- 我們從
最簡單的無入?yún)?/code>、無返參的全局block開始分析:
- (void)demo {
void(^block)(void) = ^{
NSLog(@"HT_Block");
};
block();
NSLog(@"%@",block);
}
-
為了便于
寄存器的讀取操作,我這里使用真機(jī)進(jìn)行演示,在void(^block1)(void) = ^{這一行加入斷點(diǎn),打開匯編模式,運(yùn)行代碼至斷點(diǎn)處:
image.png -
當(dāng)前讀取的是
全局Block
image.png 我們給
block添加入?yún)?/code>,讓block捕獲外界變量:
- (void)demo {
NSArray * arr = @[@"1"];
void(^block)(void) = ^{
NSLog(@"HT_Block %@",arr); // 捕獲外界變量:arr
};
block();
NSLog(@"%@",block);
}
-
斷點(diǎn)位置不變,運(yùn)行代碼,進(jìn)入?yún)R編頁面后,斷點(diǎn)在objc_retainBlock這行,打印x0(當(dāng)前對(duì)象):
image.png
Q: 按照
上面打印結(jié)果來說,有外界變量,沒有修飾符時(shí),應(yīng)該是堆區(qū)Block,為什么這里是棧區(qū)Block?
A:Block從棧區(qū)拷貝到堆區(qū),是_Block_copy操作的。(我們往下看)
-
往下驗(yàn)證,加入
objc_retainBlock符號(hào)斷點(diǎn),往下執(zhí)行進(jìn)入objc_retainBlock函數(shù)內(nèi)部:
image.png -
再次讀取
x0,發(fā)現(xiàn)此時(shí)還沒變。我們繼續(xù)control+ 鼠標(biāo)左鍵,點(diǎn)擊進(jìn)入按鈕。不斷使用register read x0和p來讀取、打印當(dāng)前對(duì)象:
image.png -
發(fā)現(xiàn)進(jìn)入
_Block_copy時(shí)是棧區(qū)Block,但是return出來時(shí),卻變成了堆區(qū)Block:
image.png 所以
block真正從棧區(qū)拷貝到堆區(qū),是_Block_copy進(jìn)行的。
回顧上面
Block基礎(chǔ)結(jié)構(gòu)梳理圖,我們可以通過Block對(duì)象的首地址進(jìn)行內(nèi)存平移獲取到invoke的。
-
通過
Block對(duì)象的首地址偏移,取到了_block_invoke:
image.png -
control + 鼠標(biāo)左鍵,點(diǎn)擊進(jìn)入,發(fā)現(xiàn)_block_invoke內(nèi)部就是block的函數(shù)執(zhí)行內(nèi)容。
image.png
【深入探究】
- 既然我們可以通過
內(nèi)存平移,取到invoke函數(shù)。那順便可以驗(yàn)證上面Block基礎(chǔ)結(jié)構(gòu)梳理圖的完整結(jié)構(gòu):- 在
objc_retainBlock之后,加斷點(diǎn),此時(shí)完成了_Block_copy操作,完成了棧區(qū)到堆區(qū)的block拷貝:
image.png對(duì)照右邊圖,慢慢看吧 ?? 整個(gè)結(jié)構(gòu)非常清晰。
image.pngblock的類型是@?,簽名中包含參數(shù)個(gè)數(shù),入?yún)?/code>和返參的具體內(nèi)容和占用內(nèi)存大小。
3.3 _Block_copy
- 進(jìn)入
源碼。搜索_Block_copy:
// Copy, or bump refcount, of a block. If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
// block都是`Block_layout`類型
struct Block_layout *aBlock;
// 沒有內(nèi)容,直接返回空
if (!arg) return NULL;
// The following would be better done as a switch statement
// 將內(nèi)容轉(zhuǎn)變?yōu)閌Block_layout`結(jié)構(gòu)體格式
aBlock = (struct Block_layout *)arg;
// 檢查是否需要釋放
if (aBlock->flags & BLOCK_NEEDS_FREE) {
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 如果是全局Block,直接返回
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
//
else {
// Its a stack block. Make a copy.
// 進(jìn)入的是棧區(qū)block,拷貝一份
// 開辟一個(gè)大小空間的result對(duì)象
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
// 開辟失敗,就返回
if (!result) return NULL;
// 內(nèi)存拷貝:將aBlock內(nèi)容拷貝到result中
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
//result的invoke指向aBlock的invoke。
result->invoke = aBlock->invoke;
#endif
// reset refcount
// BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING :前16位都為1
// ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING):前16位都為0
// 與操作,結(jié)果為前16位都為0 應(yīng)用計(jì)數(shù)為0
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
// 設(shè)置為需要釋放,引用計(jì)數(shù)為1
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// 生成desc,并記錄了result和aBlock
_Block_call_copy_helper(result, aBlock); //
// Set isa last so memory analysis tools see a fully-initialized object.
// 設(shè)置isa為堆區(qū)Block
result->isa = _NSConcreteMallocBlock;
return result;
}
}
- 如果block
需要釋放(表示已經(jīng)在堆區(qū)),就增加引用計(jì)數(shù)- 如果是
全局Block,直接返回Block_layout結(jié)構(gòu)的aBlock- 其他情況,都是從
棧區(qū)拷貝到堆區(qū):
malloc申請(qǐng)空間->memove內(nèi)存拷貝->invoke指針拷貝->
flag引用計(jì)數(shù)設(shè)為1->生成desc-> 設(shè)置isa為堆Block-> 返回堆區(qū)Block
4. block的參數(shù)處理(多次拷貝)
Q:
block對(duì)外界捕獲變量怎么管理的?有哪些操作?
- 以
__block修飾的NSArray為例,測試代碼如下:
- (void)demo {
__block NSArray * arr = @[@"1"];
void(^block)(void) = ^{
NSLog(@"HT_Block %@",arr); // 捕獲外界變量:arr
};
block();
NSLog(@"%@",block);
}
- 使用
clang將ViewController.m文件生成ViewController.cpp文件,分析:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

- 進(jìn)入
源碼,搜索_Block_object_assign:

不同
枚舉類型和組合,有不同的引用方式,其中最復(fù)雜的,是_Block_copy和_Block_byref_copy。-
_Block_copy上面已分析過了,我們搜索_Block_byref_copy:
image.png
重點(diǎn):
此處有
2次拷貝+更深層次拷貝:
- 【第一次拷貝】:
Block自身(拷貝一份到)->Block_byref結(jié)構(gòu)體(src棧區(qū)結(jié)構(gòu)體 )- 【第二次拷貝】:
src棧區(qū)結(jié)構(gòu)體 (拷貝一份到)->copy堆區(qū)結(jié)構(gòu)體- 【更深次拷貝】:調(diào)用
byref_keep方法,內(nèi)部又執(zhí)行_Block_object_assign函數(shù),再判斷是否繼續(xù)往下拷貝嵌套
- 以上,就是
Block的創(chuàng)建、調(diào)用。Block的釋放調(diào)用_Block_object_dispose函數(shù):
image.png
關(guān)于Block的探索,都在這里了。有興趣可以一個(gè)個(gè)類型去測試和監(jiān)測





















