三年前根竿,第一次寫關(guān)于 block 的東西鸿捧,就是初識(shí) block税肪,了解了些皮毛阅悍,但發(fā)現(xiàn)鸡典,那么僅僅是 block 的冰山一角套才,關(guān)于 block 還有很多需要參透和理解乎完。
block 本質(zhì)
block 的本質(zhì)是一個(gè) Objective-C 對(duì)象掰读,其內(nèi)部也有 isa 指針松逊,block 中封裝了函數(shù)的調(diào)用以及函數(shù)調(diào)用環(huán)境的 Objective-C 對(duì)象躺屁。它的結(jié)構(gòu)如下:
- 函數(shù)的調(diào)用相當(dāng)于函數(shù)的調(diào)用地址
- 函數(shù)調(diào)用環(huán)境指參數(shù),訪問(wèn) block 外部的值等
一段下面的 block:
void(^block)(int a, int b) = ^(int a, int b) {
NSLog(@"a + b = %d", a + b);
};
block(1, 2);
用命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
(下同) 重寫后 C++ 代碼是這樣的:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
__block_impl
的聲明:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
__main_block_desc_0
的聲明:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
};
Block_size
表示__main_block_impl_0
能占多少內(nèi)存棺棵。
假如 block 內(nèi)使用了外部變量楼咳,如:
int outter = 35;
void(^block)(int a, int b) = ^(int a, int b) {
NSLog(@"outter is %d", outter);
NSLog(@"a + b = %d", a + b);
};
block(1, 2);
本質(zhì)結(jié)構(gòu)為:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int outter;
};
若我們?cè)?.m
文件中自行實(shí)現(xiàn)這些結(jié)構(gòu)體:
[圖片上傳失敗...(image-e5da00-1555345050037)]
然后進(jìn)行轉(zhuǎn)換:
struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;
加斷點(diǎn)運(yùn)行后進(jìn)入 LLDB 調(diào)試環(huán)境可看到 blockStruct 的信息:
發(fā)現(xiàn) outter
已經(jīng)封裝到 blockStruct 的內(nèi)存中去了。
我們記錄下 __FuncPtr
后面的內(nèi)存地址 0x0000000100000ee0烛恤,然后在 block 塊內(nèi)增加斷點(diǎn)并過(guò)掉當(dāng)前斷點(diǎn)母怜,當(dāng)程序停留在 block 塊內(nèi)的斷點(diǎn)的時(shí)候,然后 Debug -> Debug Workflow -> Always Show Disassembly 會(huì)看到如下界面:
[圖片上傳失敗...(image-fe45c2-1555345050037)]
第一行 0x100000ee0 <+0>: pushq %rbp
的地址就是__FuncPtr
的地址缚柏,這說(shuō)明 block 塊內(nèi)的代碼都封裝到了函數(shù)里面苹熏,這個(gè)函數(shù)的首地址(例子中的 0x0000000100000ee0)在 block 結(jié)構(gòu)體的成員結(jié)構(gòu)體 __block_impl 中。
深入探究
底層數(shù)據(jù)結(jié)構(gòu)
在 main
函數(shù)中例子的代碼 C++ 的實(shí)現(xiàn)為:
int outter = 35;
// 定義 block 變量
void(*block)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, outter));
// 執(zhí)行 block 內(nèi)部代碼
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
去除強(qiáng)制轉(zhuǎn)換的干擾代碼币喧,簡(jiǎn)化后:
int outter = 35;
// 定義 block 變量
void(*block)(int a, int b) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, outter));
// 執(zhí)行 block 內(nèi)部代碼
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
這里的 block
會(huì)指向什么轨域?首先得明白 _main_block_impl_0()
會(huì)返回什么?我們?cè)?.cpp
文件中發(fā)現(xiàn)該函數(shù)在 __main_block_impl_0
的結(jié)構(gòu)體中:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int outter;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _outter, int flags=0) : outter(_outter) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
該函數(shù)接收 4 個(gè)參數(shù)杀餐,flags
默認(rèn)為 0干发,并且函數(shù)名和結(jié)構(gòu)體名相同,是 C++ 中的構(gòu)造函數(shù)史翘,和 Java 的構(gòu)造函數(shù)道理類似枉长,也和 Objective-C 中的 init
方法類似,并且無(wú)任何返回琼讽。
outter(_outter)
表示傳進(jìn)來(lái)的 _outter 的值會(huì)賦給結(jié)構(gòu)體成員變量 outter必峰,相當(dāng)于outter = _outter
。
4 個(gè)傳入的參數(shù)中 outter 不必多言钻蹬,那么來(lái) __main_block_func_0
是什么:
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int outter = __cself->outter;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_f61ac8_mi_0, outter);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_f61ac8_mi_1, a + b);
}
可見(jiàn)吼蚁,__main_block_func_0
封裝了 block 執(zhí)行邏輯的函數(shù)。__main_block_func_0 對(duì)應(yīng) __main_block_impl_0
構(gòu)造方法中的 void *fp
, fp 賦值給了 impl.FuncPtr
问欠。這樣 impl.FuncPtr 存儲(chǔ)的就是執(zhí)行邏輯的函數(shù)的地址肝匆。
在該構(gòu)造方法中同時(shí)初始化了 isa 指針:
impl.isa = &_NSConcreteStackBlock;
說(shuō)明 block 的類型為 _NSConcreteStackBlock
粒蜈。
回過(guò)頭我們?cè)倏磦魅氲牡诙€(gè)參數(shù) &__main_block_desc_0_DATA
,有關(guān)它的完整代碼為:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
該處 0 賦值給了 reserved
术唬,sizeof(struct __main_block_impl_0)
計(jì)算該 block 結(jié)構(gòu)體的大小并將結(jié)果賦值給 Block_size
薪伏。
&__main_block_desc_0_DATA
對(duì)應(yīng) __main_block_impl_0
構(gòu)造方法中的 struct __main_block_desc_0 *desc
, 并賦值給了 Desc
。換而言之 __main_block_impl_0 中的 Desc 指向的是 __main_block_desc_0 結(jié)構(gòu)體變量粗仓。
所以在執(zhí)行結(jié)構(gòu)體的構(gòu)造函數(shù)的時(shí)候嫁怀,outter 為 35。倘若在外部將 outter 重新賦值借浊,結(jié)構(gòu)體中的 outter 是不會(huì)更改的塘淑。也就是說(shuō) outter 是以值傳遞的形式傳遞的。
__main_block_func_0
中:
int outter = __cself->outter;
該步驟為取出 outter 的值(35)蚂斤。
block 的變量捕獲
為確保 block 能正確訪問(wèn)外部變量存捺,block 有變量捕獲機(jī)制,如下圖:
auto: 局部變量默認(rèn)是 auto 修飾的:
int a = 0;
等價(jià)于auto int a = 0;
曙蒸,它表示自動(dòng)變量捌治,離開(kāi)作用域后自動(dòng)銷毀。
那么 block 中的捕獲是什么意思纽窟?就是 block 內(nèi)部會(huì)新增一個(gè)成員變量用來(lái)存儲(chǔ)外部變量的值肖油,這個(gè)過(guò)程為捕獲。
auto 修飾的變量
上一節(jié)例子中的 int 型 outter 就是自動(dòng)變量臂港,默認(rèn) auto 修飾森枪。其訪問(wèn)方式是值傳遞。
static 修飾的變量
我們添加靜態(tài)變量 outter2:
int outter = 35;
static int outter2 = 1210;
void(^block)(int a, int b) = ^(int a, int b) {
NSLog(@"outter is %d", outter);
NSLog(@"outter2 is %d", outter2);
NSLog(@"a + b = %d", a + b);
};
block(10, 20);
運(yùn)行后打印了 outter 和 outter2 的值审孽。說(shuō)明無(wú)論是 auto 修飾還是 static 修飾的外部變量县袱,block 內(nèi)部都是能捕獲到的。
那么內(nèi)部訪問(wèn)方式是否一樣佑力?重寫 C++ 代碼后發(fā)現(xiàn):
void(*block)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, outter, &outter2));
outter2 是以 &outter2
傳入 __main_block_impl_0
結(jié)構(gòu)體的構(gòu)造方法的式散,并且 __main_block_impl_0 中的 outter2 是:
struct __main_block_impl_0 {
...
int outter;
int *outter2;
...
};
發(fā)現(xiàn)這里的 outter2 是通過(guò)傳址的方式傳進(jìn)去的,在打印的 C++ 實(shí)現(xiàn)中 outter2 是這樣取值的:
NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_e5faae_mi_1, (*outter2));
*outter2
這樣的取值方式是直接取出外面靜態(tài)變量?jī)?nèi)存里的值打颤。
Q:為什么會(huì)有這樣的差異杂数?
A:因?yàn)?auto 修飾的變量是可能自動(dòng)銷毀的,而 block 執(zhí)行的時(shí)機(jī)未定瘸洛,所以存在 block 執(zhí)行內(nèi)部代碼的時(shí)候變量已經(jīng)銷毀的情況,這會(huì)導(dǎo)致程序的 Crash次和,所以外部變量需進(jìn)行值傳遞反肋。而 static 修飾的變量會(huì)一直存在于內(nèi)存當(dāng)中,不存在 block 執(zhí)行的時(shí)候變量已經(jīng)銷毀的情況踏施。
全局變量
我們驗(yàn)證全局變量的捕獲機(jī)制石蔗,添加一個(gè)全局的成員變量 outter3:
int static outter3 = 1314;
并在內(nèi)部打印罕邀,發(fā)現(xiàn)打印 1314,若在執(zhí)行 block 之前修改 outter3 的值:
void(^block)(int a, int b) = ^(int a, int b) {
NSLog(@"outter is %d", outter);
NSLog(@"outter2 is %d", outter2);
NSLog(@"outter3 is %d", outter3);
NSLog(@"a + b = %d", a + b);
};
outter3 = 999;
block(10, 20);
打印得 outter3 = 999养距,看似和局部靜態(tài)變量的道理一樣诉探,我們看下 C++ 實(shí)現(xiàn)得 __main_block_impl_0 結(jié)構(gòu)體:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int outter;
int *outter2;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _outter, int *_outter2, int flags=0) : outter(_outter), outter2(_outter2) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
發(fā)現(xiàn)并無(wú) outter3,在 block 內(nèi)部打印的地方為:
NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_147262_mi_2, outter3);
也就是說(shuō)棍厌,全局變量并沒(méi)有捕獲到 block 內(nèi)部肾胯,而且,內(nèi)部訪問(wèn)全局變量是直接訪問(wèn)的耘纱。
那么假如 block 中是如何捕獲 self 的呢敬肚?
我們新建 Test
類:
.h:
@interface Test : NSObject
@property(nonatomic, copy) NSString* param;
- (void)test;
- (instancetype)initWithParam:(NSString*)param;
@end
.m:
@implementation Test
- (void)test {
void(^block)(void) = ^{
NSLog(@"====>%p", self);
};
block();
}
- (instancetype)initWithParam:(NSString*)param
{
self = [super init];
if (self) {
self.param = param;
}
return self;
}
@end
外部調(diào)用 test()
方法便可執(zhí)行 block,并訪問(wèn)內(nèi)部的 self束析。打友蘼:
====>0x10070be20
重寫 Test.m 文件后發(fā)現(xiàn)其 block 結(jié)構(gòu)為:
struct __Test__test_block_impl_0 {
struct __block_impl impl;
struct __Test__test_block_desc_0* Desc;
Test *self;
__Test__test_block_impl_0(void *fp, struct __Test__test_block_desc_0 *desc, Test *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
發(fā)現(xiàn) self 是通過(guò)指針傳遞進(jìn)來(lái)的。而且可推導(dǎo)员寇,既然能捕獲弄慰,說(shuō)明 self 是局部變量。
我們可看到 test() 函數(shù)的底層為:
static void _I_Test_test(Test * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__Test__test_block_impl_0((void *)__Test__test_block_func_0, &__Test__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
發(fā)現(xiàn)底層函數(shù)默認(rèn)添加了兩個(gè)參數(shù):self
和 _cmd
蝶锋,這也是我們?yōu)槭裁茨茉诤瘮?shù)內(nèi)可以調(diào)用到 self 和 _cmd 的原因陆爽。
若假如打印 _param 呢?運(yùn)行
Test* t = [[Test alloc] initWithParam:@"something"];[t test];
發(fā)現(xiàn)可打由馈:
something
此時(shí)是捕獲的 _param墓陈?錯(cuò),_param 等價(jià)于 self->_param
第献,所以捕獲的還是 self贡必。
block 的類型
block 有三種類型,亦是可以通過(guò) class
方法或者查看 isa 指針查看其具體類型庸毫,但最終都是繼承自 NSBlock
仔拟。
類型 | |
---|---|
_NSGlobalBlock_ | 全局 block |
_NSStackBlock_ | 棧區(qū) block |
_NSMallocBlock_ | 堆區(qū) block |
我們?yōu)樘骄科漕愋停\(yùn)行:
void(^block)(void) = ^{
NSLog(@"This is a block");
};
block();
NSLog(@"%@", [block class]);
打屿摺:
This is a block
__NSGlobalBlock__
追加打永ā:
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
得:
__NSGlobalBlock
NSBlock
NSObject
現(xiàn)在得到繼承鏈:
那么現(xiàn)在打印:
void(^block)(void) = ^{
NSLog(@"This is a block");
};
int num = 10;
void(^block1)(void) = ^{
NSLog(@"The num is %d", num);
};
NSLog(@"%@ %@ %@", [block class], [block1 class], [^{
NSLog(@"The num is %d", num);
} class]);
得:
__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
重寫后我們可看見(jiàn)有三個(gè) block:__main_block_impl_0载佳、__main_block_impl_1炒事、__main_block_impl_2。
但奇怪的是三者得 isa 指針都指向的是 _NSConcreteStackBlock
蔫慧。
為什么會(huì)出現(xiàn)這樣的問(wèn)題挠乳,是因?yàn)樵谥貙懨钪型ㄟ^(guò) clang
轉(zhuǎn)成的 C++ 代碼并不能完全代表 Objective-C 最終的底層實(shí)現(xiàn)。
所以我們還是按照打印的標(biāo)準(zhǔn)也判斷 block 的類型,可發(fā)現(xiàn)睡扬, block 的存儲(chǔ)類型和捕獲外部的局部變量也有關(guān)系盟蚣。
text 區(qū)存放的是程序代碼,data 區(qū)存放的是全局變量卖怜,堆區(qū)放的是 alloc 出來(lái)的對(duì)象屎开,動(dòng)態(tài)分配內(nèi)存,需要開(kāi)發(fā)者手動(dòng)調(diào)用马靠,也需要開(kāi)發(fā)者主動(dòng)管理內(nèi)存(現(xiàn)在有 ARC 了)奄抽,棧區(qū)放的是局部變量,系統(tǒng)自動(dòng)銷毀內(nèi)存虑粥。
具體的 block 類型是區(qū)分的如孝?如下表:
block 類型 | 區(qū)別 |
---|---|
_NSGlobalBlock_ | 沒(méi)有訪問(wèn) auto 變量 |
_NSStackBlock_ | 訪問(wèn) auto 變量 |
_NSMallocBlock_ | _NSStackBlock_ 調(diào)用了 copy |
對(duì)于 NSStackBlock 的 block 存在一個(gè)問(wèn)題,代碼如下:
void(^block)(void);
void test() {
int num = 35;
block = ^{
NSLog(@"The num is %d", num);
};
NSLog(@"%@", [block class]);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
}
運(yùn)行結(jié)果為:
__NSStackBlock__
The num is -272632472
做這個(gè)實(shí)驗(yàn)請(qǐng)將內(nèi)存管理改為手動(dòng)(MRC):Build Setting -> Objective-C Automatic Reference Counting -> No
為何會(huì)出現(xiàn) -272632472娩贷?是因?yàn)榈谖瑘?zhí)行過(guò) test()
后棧區(qū)的 對(duì)應(yīng)數(shù)據(jù)被回收,存在的可能就是垃圾數(shù)據(jù)彬祖,那么再訪問(wèn)結(jié)構(gòu)體內(nèi)的成員的時(shí)候得到的就是這些垃圾數(shù)字茁瘦。
將上述代碼稍作改動(dòng),test 內(nèi)的 block 改為:
block = [^{
NSLog(@"The num is %d", num);
} copy];
打印結(jié)果為:
__NSMallocBlock__
The num is 35
此時(shí)的 block 已經(jīng)進(jìn)行了 copy 操作储笑,棧 block 變?yōu)槎?block甜熔,內(nèi)存需要我們手動(dòng)釋放,而我并沒(méi)有釋放突倍,所以打印的 num 是正確的腔稀。
產(chǎn)生疑惑,_NSGlobalBlock_ 類型的棧進(jìn)行了 copy 操作會(huì)變成 _NSMallocBlock_ 類型嗎羽历?
去掉 block 內(nèi)部對(duì) num 的打印再來(lái)運(yùn)行發(fā)現(xiàn):
__NSGlobalBlock__
即使使用了 copy 操作焊虏,block 依然為 _NSGlobalBlock_ 類型。
copy 操作
由上節(jié)可知秕磷,對(duì)于 _NSStackBlock_ 類型的 block 有太多的不確定性诵闭,所以在對(duì)這種 block 使用的時(shí)候需要對(duì)其進(jìn)行一次 copy
操作將棧 block 復(fù)制到堆區(qū)。
但上節(jié)的例子是基于 MRC 的環(huán)境下操作的澎嚣,在 ARC 的環(huán)境下疏尿,編譯器會(huì)根據(jù)情況自動(dòng)講 block 進(jìn)行 copy 操作。
在 ARC 環(huán)境下執(zhí)行:
void(^block)(void);
void test() {
int num = 35;
block = ^{
NSLog(@"The num is %d", num);
};
NSLog(@"%@", [block class]);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
}
得:
__NSMallocBlock__
The num is 35
在以下條件下易桃,編譯器會(huì)自動(dòng)將 block 進(jìn)行 copy 操作:
- block 作為返回值
- 將 block 復(fù)制給 __strong 指針時(shí)
-
block 作為 Cocoa API 中方法名含有 usingBlock 的方法參數(shù)時(shí)
如:
NSArray* arr = ...;
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
對(duì)象類型的 auto 變量捕獲
前面的例子內(nèi)部捕獲外部變量都是基本類型褥琐,如 int,那么對(duì)象類型的外部變量是如何捕獲的晤郑?
將 Test 類踩衩,添加 NSInteger 類型的屬性 num嚼鹉。
外部:
Test* test = [[Test alloc] init];
test.num = 35;
TestBlock block = ^{
NSLog(@"The num is %ld", (long)test.num);
};
block();
TestBlock 定義為:typedef void(^TestBlock) (void);
運(yùn)行得:
The num is 35
我們稍作改動(dòng):
TestBlock block;
{
Test* test = [[Test alloc] init];
test.num = 35;
block = ^{
NSLog(@"The num is %ld", (long)test.num);
};
}
NSLog(@"=====end=====");
也重寫了 Test 的 dealloc()
方法打印 dealloc。我們?cè)黾訑帱c(diǎn)在打印 “end” 的一行驱富,運(yùn)行發(fā)現(xiàn)斷點(diǎn)處,并沒(méi)有打印 Test 的 delloc 信息匹舞,也就是說(shuō)褐鸥,內(nèi)部 {}
執(zhí)行完了 Test 也沒(méi)有立即被銷毀。
我們將代碼改成:
Test* test = [[Test alloc] init];
test.num = 35;
TestBlock block = ^{
NSLog(@"The num is %ld", (long)test.num);
};
NSLog(@"=====end=====");
重寫后發(fā)現(xiàn) block 的結(jié)構(gòu)體中有 Test *test
成員變量赐稽〗虚牛回到修改之前的代碼,在執(zhí)行:
block = ^{
NSLog(@"The num is %ld", (long)test.num);
};
的時(shí)候姊舵,block 進(jìn)行了 copy 操作成為堆區(qū)的 block晰绎,不會(huì)輕易銷毀,那么意味著對(duì) test 也是強(qiáng)引用持有括丁,test 亦不會(huì)輕易被釋放荞下,所以 dealloc 信息延后打印:
=====end=====
=====dealloc=====
若是 MRC 環(huán)境(需添加 [t release]
操作史飞,并且 dealloc 方法內(nèi)須調(diào)用父類的 dealloc 方法)尖昏,即使 block 還在,也會(huì)先執(zhí)行 Test 的 dealloc 方法构资。結(jié)果為:
=====dealloc=====
=====end=====
若在 MRC 環(huán)境下改為:
block = [^{
NSLog(@"The num is %ld", (long)test.num);
} copy];
則會(huì)達(dá)到 ARC 下同樣的效果抽诉,因?yàn)檫M(jìn)行了 copy 操作后在 block 內(nèi)部相當(dāng)于調(diào)用了一次 [t reatain]
操作。結(jié)果為:
=====end=====
=====dealloc=====
回到 ARC 環(huán)境吐绵,假如 Test 對(duì)象進(jìn)行 __weak 修飾迹淌,則情況又有所不同:
=====dealloc=====
=====end=====
在用 __weak 修飾的情況下重寫 C++ 代碼會(huì)報(bào)錯(cuò):
cannot create __weak reference because the current deployment target does not support weak references
是因?yàn)?strong>命令需要支持 ARC 并且指定運(yùn)行時(shí)系統(tǒng)版本,如:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m
重寫成功后發(fā)現(xiàn) block 結(jié)構(gòu)體為:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Test *__weak test;
...
};
test 對(duì)象為 weak
修飾己单,所以在離開(kāi)作用域后立即釋放唉窃。去掉 weak 后的結(jié)構(gòu)體再用上命令重寫,得到:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Test *__strong test;
...
};
發(fā)現(xiàn) weak 默認(rèn)用了 strong
修飾荷鼠,所以“延長(zhǎng)了”其壽命句携。
最后來(lái)個(gè)總結(jié):
- 當(dāng) block 在棧上,不會(huì)對(duì) auto 變量產(chǎn)生強(qiáng)引用
- 當(dāng) block 在堆上允乐,會(huì)根據(jù) auto 是否由 __strong 或者 —__weak 修飾來(lái)決定是否產(chǎn)生強(qiáng)引用 [下有說(shuō)明]
- 當(dāng) block 從堆上移除矮嫉,將放棄對(duì) auto 變量的引用,相當(dāng)于進(jìn)行了一次
release
操作
copy 操作后的 block 其 Desc
是有變化的:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
}__main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
原本只有 reserved
和 Block_size
現(xiàn)在又多了兩個(gè)函數(shù)指針: copy
和 dispose
牍疏。copy 保存的是 __main_block_copy_0
蠢笋,dispose 保存的是 __main_block_dispose_0
。
當(dāng) block 執(zhí)行了 copy 操作后鳞陨,這兩個(gè)函數(shù)便會(huì)執(zhí)行昨寞。
__main_block_copy_0 和 __main_block_dispose_0是現(xiàn)實(shí):
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
// 會(huì)根據(jù) test 對(duì)象是 strong 還是 weak 修飾來(lái)決定是否對(duì) test 對(duì)象產(chǎn)生強(qiáng)引用
_Block_object_assign((void*)&dst->test, (void*)src->test, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
// 對(duì) test 對(duì)象進(jìn)行釋放
_Block_object_dispose((void*)src->test, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
函數(shù) | 調(diào)用時(shí)機(jī) |
---|---|
copy | 棧上的 block 復(fù)制到堆時(shí) |
dispose | 堆上的 block 被收回時(shí) |
__block
我們?cè)賮?lái)新建一個(gè)例子工程:
typedef void(^TestBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int num = 10;
TestBlock block = ^{
NSLog(@"The num is %d", num);
};
block();
}
return 0;
}
運(yùn)行上面這段代碼瞻惋,結(jié)果為:
The num is 10
那么實(shí)際情況中,我們常常需要在 block 內(nèi)部改變外面變量的值援岩,在 block 內(nèi)部直接修改是不允許的:
^{
num = 35; // ?
}
這是因?yàn)?num 的作用域?qū)儆?main 函數(shù)歼狼,而 block 內(nèi)執(zhí)行邏輯屬于另一個(gè)函數(shù) __main_block_func_0
,是無(wú)法跨域進(jìn)行修改的享怀。
但是通過(guò) static
修飾的局部變量是可以用這種方式修改的:
static int num = 10;
TestBlock block = ^{
num = 35;
NSLog(@"The num is %d", num);
};
block();
結(jié)果為:
The num is 35
因?yàn)?static 修飾的是引用傳遞羽峰,block 的結(jié)構(gòu)體存儲(chǔ)的是指向 num 的指針,所以在內(nèi)部修改 num 的值是可以成功的添瓷。
那么如何修改非 static 修飾的的局部變量梅屉?就是 __block
關(guān)鍵字。
__block int num = 10;
TestBlock block = ^{
num = 35;
NSLog(@"The num is %d", num);
};
block();
結(jié)果:
The num is 35
__block 本質(zhì)
__block 變量不能修飾全局變量鳞贷、靜態(tài)變量坯汤。并且編譯器會(huì)將 __block 變量包裝成一個(gè)對(duì)象。
重寫 C++ 代碼后發(fā)現(xiàn) block 結(jié)構(gòu)體 num 的成員變量和之前未用 __block 修飾的 num 有本質(zhì)的區(qū)別:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_num_0 *num; // by ref
...
};
這里的 num 為 __Block_byref_num_0 *
類型搀愧。__Block_byref_num_0 也是個(gè)結(jié)構(gòu)體惰聂,其內(nèi)部定義是這樣的:
struct __Block_byref_num_0 {
void *__isa;
__Block_byref_num_0 *__forwarding;
int __flags;
int __size;
int num;
};
我們可推斷一開(kāi)始 num 的值為 10,這個(gè)值一定是存儲(chǔ)在 __Block_byref_num_0 的成員變量 num 中妈橄。那么 __forwarding
表示什么庶近?
首先我們看到由 __block 修飾后的 num,在 main 函數(shù)的源碼中變成了:
__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};
簡(jiǎn)化版本:
__Block_byref_num_0 num = {(0,
&num,
0,
sizeof(__Block_byref_num_0),
10};
此時(shí)第一個(gè) 0 賦值給 __isa眷蚓,第二個(gè) 0 賦值給 __flags鼻种,第四個(gè)參數(shù)是計(jì)算當(dāng)前結(jié)構(gòu)體有多大并賦值給 __size,最后 10 賦值給 num沙热,推斷得到驗(yàn)證叉钥。第二個(gè)參數(shù) &num
就是 num 結(jié)構(gòu)體本身,也就是說(shuō)它將自身的結(jié)構(gòu)體地址傳遞給了 __forwarding篙贸。換而言之 __forwarding 指向的是自己投队。
同時(shí) &num 也傳給了 __main_block_impl_0 的 *num
:
TestBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
block 修改 num 的源碼為:
// 首先拿到 __Block_byref_num_0 中的 __forwarding
__Block_byref_num_0 *num = __cself->num;
// 取得 num 再修改
(num->__forwarding->num) = 35;
倘若多加了一個(gè)對(duì)象類型的局部變量:
__block int num = 10;
__block NSObject* obj = [[NSObject alloc] init];
TestBlock block = ^{
obj = nil;
num = 35;
NSLog(@"The num is %d", num);
};
block();
num 和 obj 在底層會(huì)生成兩個(gè)機(jī)構(gòu)體:
struct __Block_byref_num_0 {
void *__isa;
__Block_byref_num_0 *__forwarding;
int __flags;
int __size;
int num;
};
struct __Block_byref_obj_1 {
void *__isa;
__Block_byref_obj_1 *__forwarding;
int __flags;
int __size;
// copy 操作
void (*__Block_byref_id_object_copy)(void*, void*);
// dispose 操作
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong obj;
};
block 結(jié)構(gòu)體會(huì)有兩個(gè)成員變量指向它們?cè)谶@里不貼出。
我們?nèi)サ魧?duì)象類型的 obj 回到最簡(jiǎn)狀態(tài)爵川,在 block()
后打印 num 的內(nèi)存地址敷鸦,得:
0x10051e968
這個(gè)內(nèi)存地址和底層的誰(shuí)有對(duì)應(yīng)關(guān)系?是 __main_block_impl_0 中的 *num寝贡?還是 __Block_byref_num_0 中的 num扒披?我們自己實(shí)現(xiàn)這些低層結(jié)構(gòu):
然后運(yùn)行:
__block int num = 10;
TestBlock block = ^{
num = 35;
NSLog(@"The num is %d", num);
};
struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;
NSLog(@"%p", &num);
在最后一行加斷點(diǎn)發(fā)現(xiàn) __Block_byref_num_0 * 型 num 的地址為:0x000000010204b490,打印局部變量的 num 為 0x10204b4a8圃泡,兩者并不相同碟案。
0x000000010204b490 為 __Block_byref_num_0 * 型 num 的地址也就意味著是 __isa 的地址,那么 age 的地址是什么颇蜡?
__isa 大小為 8价说,__forwarding 大小為 8(地址為 0x000000010204b498)辆亏,__flags 大小為 4(地址為0x000000010204b4a0), __size 大小為 4(地址為0x000000010204b4a4)鳖目,num 的地址為 0x000000010204b4a8扮叨。是不是很眼熟?沒(méi)錯(cuò) num 的地址和外部變量的 num 一樣领迈。
通過(guò):
print/x &(blockStruct->num->num)
命令得到的打印結(jié)果和 NSLog(@"%p", &num);
得到的結(jié)果也是一樣的也可以驗(yàn)證甫匹。
__block 內(nèi)存管理
我們來(lái)看這個(gè)熟悉的例子:
int num = 0;
TestBlock block = ^{
NSLog(@"%d", num);
};
block();
底層的 __main_block_desc_0
是沒(méi)有 copy
和 dispose
兩個(gè)成員函數(shù)的,但是當(dāng) num 用 __block 的時(shí)候就多了這兩個(gè)函數(shù)惦费,并在 copy 函數(shù)中調(diào)用 _Block_object_assign()
對(duì) 結(jié)構(gòu)體中的 __Block_byref_num_0 *num
進(jìn)行內(nèi)存管理。
假如有 Block 0 和 Block 1 分別對(duì) __block 變量引用抢韭,則:
在 ARC 環(huán)境下首先 Block 0 會(huì) copy 到堆上薪贫,然后 __block 修飾的變量也同樣會(huì) copy 到堆上,然后進(jìn)行強(qiáng)引用刻恭。
然后 Block 1 也會(huì) copy 到堆上并對(duì) __block 變量有強(qiáng)引用:
當(dāng) block 從堆上移除的時(shí)候瞧省,首先會(huì)調(diào)用內(nèi)部 dispose 函數(shù),其內(nèi)部會(huì)調(diào)用 _Block_object_dispose()
函數(shù)鳍贾,然后釋放 __block 變量:
若外部是:
__block int num = 0;
__block NSObject* obj == ...;
TestBlock block = ^{
...
};
block();
則底層對(duì) int 和 obj 都會(huì)產(chǎn)生強(qiáng)引用鞍匾。
_Block_byref名字_0 就是強(qiáng)引用
若:
__block int num = 0;
NSObject* obj = [[NSObject alloc] init];
__weak NSObject* weakObj = obj;
TestBlock block = ^{
...
};
block();
則底層不會(huì)對(duì) weakObj 產(chǎn)生強(qiáng)引用。
另骑科,我們?cè)?C++ 代碼中看到:
__main_block_impl_0*src) {
_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
8 表示 __block 修飾的變量橡淑,對(duì)應(yīng)注釋:BLOCK_FIELD_IS_BYREF
3 表示對(duì)象,對(duì)應(yīng)注釋:BLOCK_FIELD_IS_OBJECT
__block 的 __forwarding 指針
當(dāng) block 在棧上時(shí)咆爽,__forwarding 指針指向自己梁棠。那么堆上的 __forwarding 指向誰(shuí)呢?答案也是自己斗埂,但是需要注意的是符糊,經(jīng)過(guò) copy 操作后,原棧上的 __forwarding 指針指向堆上的 block呛凶,即:
循環(huán)引用問(wèn)題
當(dāng)對(duì)象對(duì) block 本身有強(qiáng)引用男娄,而 block 又對(duì)對(duì)象持有,則會(huì)引發(fā)循環(huán)引用漾稀。如:
Test* t = [[Test alloc] init];
t.num = 35;
t.block = ^{
NSLog(@"%ld", t.num);
};
ARC
使用 __weak 和 __unsafe_unretained 解決
在 ARC 環(huán)境下可通過(guò)模闲,__weak
和 __unsafe_unretained
解決:
Test* t = [[Test alloc] init];
t.num = 35;
__weak Test* weakT = t;
t.block = ^{
NSLog(@"%ld", weakT.num);
};
或者:
Test* t = [[Test alloc] init];
t.num = 35;
__weak typeof(t) weakT = t;
t.block = ^{
NSLog(@"%ld", weakT.num);
};
對(duì)于 self 的情況也是同理:
__weak typeof(self) weakSelf = self;
__unsafe_unretained 同理,但 __unsafe_unretained 是不安全的县好,若 __weak 指向的對(duì)象銷毀围橡,則 weakXXX 會(huì)自動(dòng)置為 nil
,但 __unsafe_unretained 不會(huì)缕贡,它還是會(huì)指向那個(gè)銷毀對(duì)象的地址翁授,所以進(jìn)行訪問(wèn) weakXXX 的時(shí)候很有可能產(chǎn)生野指針錯(cuò)誤拣播。
使用 __block 解決
__block 情況下的循環(huán)應(yīng)用如下:
在必須調(diào)用 block 的情況下還可以使用 __block 來(lái)解決。
__block id weakSelf = self;
并且 block 內(nèi)部的 weakSelf 要職位 nil:
xxx.block = ^{
...
weakSelf = nil;
};
因?yàn)橐坏?weakSelf 置為 nil收擦,三者互相“僵持不下”的狀態(tài)就會(huì)打破贮配,也就不存在循環(huán)引用的問(wèn)題了。
MRC
使用 __unsafe_unretained 解決
同 ARC 環(huán)境的方式一樣塞赂。
MRC 下不支持 __weak泪勒。
使用 __block 解決
在 MRC 環(huán)境下使用 __block 修飾的話在底層是不會(huì)對(duì)外部變量進(jìn)行 retain 也就是強(qiáng)引用操作的,而 ARC 會(huì)宴猾。
并且不需要調(diào)用 weakSelf = nil
就可以解決循環(huán)引用的問(wèn)題圆存。