_我們先通過一個(gè)小場(chǎng)景,開始今天的主題.

有過開發(fā)經(jīng)驗(yàn)的同學(xué)都知道,在block內(nèi)部是無法修改局部變量的,為什么不能修改呢?我們從底層一探究竟,我們把這段代碼轉(zhuǎn)成
C++ ,查看一下底層:
如圖所示,是無法修改的,那怎么樣才能在
block內(nèi)部修改外部變量呢?有三種方法:1:使用
static修飾age2:把
age變成全局變量3:使用
__block修飾age前兩種方法我們就不細(xì)說了,在Block如何捕獲外部變量一:基本數(shù)據(jù)類型中已經(jīng)說的很明白了,并且前兩種方法會(huì)一直保存在內(nèi)存中占用內(nèi)存.我們重點(diǎn)研究
__block關(guān)鍵字.
一:__block的本質(zhì)
我們把使用__block修飾的age轉(zhuǎn)換為C++代碼,對(duì)比一下:

可以發(fā)現(xiàn):使用
__block修飾的age底層被轉(zhuǎn)換成了一個(gè)__Block_byref_age_0對(duì)象,我們重點(diǎn)研究一下這個(gè)對(duì)象.我們找到main函數(shù)中聲明__block int age = 10這句代碼的底層代碼:
之前沒有使用
__block修飾時(shí),底層代碼就是int age = 10,使用了__block修飾后,會(huì)發(fā)現(xiàn)變化如此之大,我們對(duì)這句轉(zhuǎn)換后的代碼簡化一下:
ok,現(xiàn)在我們來梳理一下使用__block修飾的age變量和block的關(guān)系,為此,我截了一張 OC 代碼和轉(zhuǎn)換后的 C++ 代碼的對(duì)比圖,這樣能更清晰的展示他們之間的關(guān)系:

這張圖和清晰的展示了使用
__block修飾的age變量和block的關(guān)系,我們思考一下在block內(nèi)部是如何修改age的值得,我們還是通過底層代碼查看:
從截圖中可以看到,
block內(nèi)部修改auto變量,是先通過參數(shù)傳遞進(jìn)來的block找到age結(jié)構(gòu)體,然后通過age結(jié)構(gòu)體找到__forwarding成員,通過之前的分析已經(jīng)知道__forwarding存儲(chǔ)的指針指向age結(jié)構(gòu)體自己,所以本質(zhì)上還是通過age結(jié)構(gòu)體找到存儲(chǔ)auto變量值得age成員,然后修改成 20.
思考一:為什么蘋果要設(shè)計(jì)forwarding這種多此一舉的方式呢?
因?yàn)楫?dāng)block從棧上拷貝到堆上后,__block變量也會(huì)拷貝到堆上.這時(shí)就有兩份__block變量,一份棧上的,一份堆上的.而棧上的__block變量隨時(shí)可能銷毀,訪問時(shí)可能出現(xiàn)野指針情況,為了保證始終訪問同一份有效且安全的數(shù)據(jù),需要把棧上__block的forwarding指針指向堆上__block地址.這樣就能保證即使訪問棧上的__block變量也能獲取到堆上的變量值,如圖:

思考二:如果我們修改的外部變量是對(duì)象類型,它的底層是怎樣的呢?

通過對(duì)比我們發(fā)現(xiàn),對(duì)象類型也會(huì)包裝成一個(gè)結(jié)構(gòu)體,并且這個(gè)結(jié)構(gòu)體里面也會(huì)有一個(gè)成員存放
auto變量的值.區(qū)別是對(duì)象類型的結(jié)構(gòu)體里面多了copy和dispose兩個(gè)函數(shù),我們?cè)?a href="http://m.itdecent.cn/p/86fbe9dfb0f5" target="_blank">Block如何捕獲外部變量二:對(duì)象類型里面已經(jīng)講過,因?yàn)閷?duì)象類型會(huì)涉及到內(nèi)存管理問題.
思考三:如圖所示,我們?cè)?code>block內(nèi)部給外部沒有使用__block修飾的array類型的auto變量添加元素,會(huì)編譯成功嗎?

肯定會(huì)成功的,這里我們不要搞混淆了,我們給
array添加元素,是使用這個(gè)地址,而不是修改這個(gè)地址,如果我們array = nil這樣才會(huì)報(bào)錯(cuò).
思考四:通過上述分析,我們知道age結(jié)構(gòu)體中會(huì)有一個(gè)成員存儲(chǔ)auto變量的值,所以現(xiàn)在就有兩個(gè)age.一個(gè)是age 結(jié)構(gòu)體,另一個(gè)是存儲(chǔ)變量值得age成員,那我們?nèi)绻蛴?code>age的地址,會(huì)是哪個(gè)age的地址呢?

為了研究清楚這個(gè)問題,我們把底層的C++的
block結(jié)構(gòu)體挪到我們的OC代碼中,代碼如下:
typedef void(^MYBlock)(void);
//impl結(jié)構(gòu)體
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//age結(jié)構(gòu)體
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
//Desc結(jié)構(gòu)體
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};
//block結(jié)構(gòu)體
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
struct __Block_byref_age_0 *age; // by ref
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
//聲明age
__block int age = 10;
//聲明block
MYBlock block = ^{
age = 20;
NSLog(@"%p",&age);
};
struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
//調(diào)用block
block();
}
return 0;
}
這樣我們就實(shí)現(xiàn)了把Block類型轉(zhuǎn)換為struct __main_block_impl_0結(jié)構(gòu)體,方便我們查看block結(jié)構(gòu)體中成員變量的地址.


我們?cè)诤瘮?shù)返回之前設(shè)置一個(gè)斷點(diǎn),然后打印了一個(gè)地址
0x100405998,我們從變量視圖中可以直接看到block結(jié)構(gòu)體中的age結(jié)構(gòu)體的地址是0x100405980,可以看出打印的地址和age結(jié)構(gòu)體的地址并不相同.由此我們可以得出結(jié)論:打印的地址是age結(jié)構(gòu)體中的age成員地址,并不是block內(nèi)部捕獲的age.ok,我們來驗(yàn)證我們剛剛得出的結(jié)論.
- 驗(yàn)證方法一:
我們?cè)谧兞恳晥D中看到age結(jié)構(gòu)體的地址是0x100405980,而我們知道,結(jié)構(gòu)體的地址也是結(jié)構(gòu)體第一個(gè)成員的地址,由此我們可以手動(dòng)計(jì)算出結(jié)構(gòu)體中age成員的地址:
//age結(jié)構(gòu)體
struct __Block_byref_age_0 {//0x100405980 age結(jié)構(gòu)體地址
void *__isa;// 占8字節(jié) 0x100405980 第一個(gè)成員的地址和age的地址相同
__Block_byref_age_0 *__forwarding;// 占8字節(jié) isa的地址:0x100405980 + isa所占用的8字節(jié) = 0x100405988
int __flags;// 占4字節(jié) __forwarding 的地址:0x100405988 + __forwarding 所占用的8字節(jié) = 0x100405990
int __size;// 占4字節(jié) __flags地址:0x100405990 + __flags 所占用的4字節(jié) = 0x100405994
int age; // 占4字節(jié) __size 地址:0x100405994 + __size 所占用的4字節(jié) = 0x100405998
};
可以看到,我們計(jì)算處理的地址0x100405998和打印出來的地址0x100405998是相同的,驗(yàn)證了我們剛才得出的結(jié)論.
- 驗(yàn)證方法二:通過命令行打印地址:
我們通過LLDB命令p/x &(blockImpl->age->age)打印出的age地址和NSLog輸出的地址是相同的,再一次驗(yàn)證了我們剛才的結(jié)論.這張截圖中的地址和上一張圖中的地址不一樣,是因?yàn)檫@張圖是重新運(yùn)行代碼截取的,變量的內(nèi)存地址已經(jīng)改變了,大家不用糾結(jié)這里.
?我們思考一下蘋果為什么這樣設(shè)計(jì)?因?yàn)樘O果要隱藏它內(nèi)部的實(shí)現(xiàn),我們?cè)谛薷?code>__block修飾的age的值時(shí),從表面看會(huì)以為真的是在直接修改age的值,如果不了解底層實(shí)現(xiàn)的話,根本就不知道被__block修飾的age已經(jīng)被包裝成了一個(gè)對(duì)象,而我們實(shí)際修改的是age結(jié)構(gòu)體中的age成員的值.
二:__block的內(nèi)存管理
我們?cè)?a href="http://m.itdecent.cn/p/86fbe9dfb0f5" target="_blank">block如何捕獲對(duì)象類型的文章中已經(jīng)知道,如果block訪問的是對(duì)象類型的變量,那么__main_block_desc_0結(jié)構(gòu)體中會(huì)增加copy,dispose兩個(gè)函數(shù)指針,這兩個(gè)函數(shù)會(huì)根據(jù)變量的修飾符(__strong,__weak,__unretained)進(jìn)行相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用或者是釋放引用的變量.現(xiàn)在我們來研究一下,使用__block修飾的外部變量,在block內(nèi)部是如何管理的.
聲明一個(gè)__block修飾的age變量,并且在block內(nèi)部訪問它:
int main(int argc, const char * argv[]) {
@autoreleasepool {
//聲明 age 變量
__block int age = 10;
//聲明block
MYBlock myblock = ^{
NSLog(@"%d",age);
};
//調(diào)用block
myblock();
}
return 0;
}
然后我們轉(zhuǎn)成 C++ 代碼,看看block底層是如何管理的:
//desc結(jié)構(gòu)體
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};
會(huì)發(fā)現(xiàn)同訪問了對(duì)象類型的變量一樣,__main_block_desc_0結(jié)構(gòu)體中同樣有copy,dispose函數(shù),可能有人會(huì)有疑問,因?yàn)樵?a href="http://m.itdecent.cn/p/86fbe9dfb0f5" target="_blank">block如何捕獲對(duì)象類型中我們說過,只有在訪問對(duì)象類型的auto變量時(shí)才會(huì)生成copy dispose.為什么訪問 __block int 時(shí)也會(huì)產(chǎn)生呢?
在上面我們說過,block在訪問__block修飾的變量時(shí),其底層會(huì)被封裝成__Block_byref_age_0類型,在這個(gè)類型存在一個(gè)void *__isa成員,所以本質(zhì)上它就是一個(gè)對(duì)象類型.
Block內(nèi)部是如何管理使用__block修飾的變量的呢?
- 在棧上的block,不會(huì)對(duì)__block產(chǎn)生強(qiáng)引用
- 當(dāng)block拷貝到堆上時(shí)
1:會(huì)調(diào)用block內(nèi)部的copy函數(shù)
2:copy函數(shù)內(nèi)部會(huì)調(diào)用__main_block_copy_0函數(shù)
3:__main_block_copy_0會(huì)調(diào)用_Block_object_assign函數(shù)對(duì)__block變量形成強(qiáng)引用(return)
也就是說,一旦_Block_object_assign調(diào)用,就會(huì)對(duì)block內(nèi)部的__block變量產(chǎn)生強(qiáng)引用
如圖:
思考一下:__block int age = 10是存放在棧上的,而MYBlock myblock是存放在堆上的,我們使用堆上的地址去指向??臻g肯定是不行的,那block的內(nèi)部是如何處理這種情況的呢?
結(jié)論就是,當(dāng)棧上的blcok拷貝到堆上時(shí),會(huì)把__block修飾的變量一同拷貝到堆上,如圖:


而當(dāng)block從堆中移除時(shí):
1:調(diào)用block內(nèi)部的dispose函數(shù)
2:dispose函數(shù)會(huì)調(diào)用
_Block_object_dispose函數(shù)3:
_Block_object_dipose函數(shù)會(huì)自動(dòng)釋放引用的__block變量(release)如圖:


思考一下,蘋果為什么會(huì)這么設(shè)計(jì)呢?其實(shí)這個(gè)也很好理解,因?yàn)槭褂?code>__blcok修飾的變量,底層其實(shí)被封裝成了對(duì)象,而我們要在block中使用這個(gè)對(duì)象,就肯定要對(duì)這個(gè)對(duì)象的引用負(fù)責(zé).
既然 block 訪問對(duì)象類型的變量和訪問使用 __block 修飾的變量都會(huì)增加copy,dispose函數(shù),那么他們之間有沒有區(qū)別呢?
他們之間的區(qū)別就是:
- 如果使用__block修飾的變量,block內(nèi)部直接對(duì)其強(qiáng)引用
- 如果是對(duì)象類型的變量,會(huì)根據(jù)變量的修飾符
__weak , __strong來決定是否強(qiáng)引用
三:block訪問 __block 修飾的對(duì)象類型
到目前為止,我們講解了block訪問 基本數(shù)據(jù)類型 (int age) , __block 修飾的基本數(shù)據(jù)類型 (__block int age), 對(duì)象類型 NSObject *object三種情況,下面我們分析一下第四種情況: __blcok 修飾的對(duì)象類型.
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc]init];
__block Person *blockPerson = person;
MYBlock block = ^{
NSLog(@"%@",blockPerson);
};
block();
}
return 0;
}
我們創(chuàng)建一個(gè)Person類,然后使用__block修飾一個(gè)person對(duì)象,在block 中訪問,查看底層代碼如下:
// __block 底層對(duì)象
struct __Block_byref_blockPerson_0 {
void *__isa;
__Block_byref_blockPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
// __block 中強(qiáng)引用 Person 對(duì)象
Person *__strong blockPerson;
};
// block 底層對(duì)象
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// block中強(qiáng)引用 __Block_byref_blockPerson_0
__Block_byref_blockPerson_0 *blockPerson; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_blockPerson_0 *_blockPerson, int flags=0) : blockPerson(_blockPerson->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// block 代碼塊
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_blockPerson_0 *blockPerson = __cself->blockPerson; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_ea4426_mi_0,(blockPerson->__forwarding->blockPerson));
}
可以看到使用__block修飾的對(duì)象底層被封裝成了__Block_byref_blockPerson_0類型的對(duì)象,__Block_byref_blockPerson_0這個(gè)對(duì)象類型的結(jié)構(gòu)體中有一個(gè)Person *__strong blockPerson成員,強(qiáng)引用著我們?cè)?code>main函數(shù)中創(chuàng)建的Person對(duì)象,而__main_block_impl_0中的blockPerson又強(qiáng)引用著__Block_byref_blockPerson_0對(duì)象,他們的關(guān)系如下圖:

我們稍作修改,在
blockPerson添加__weak關(guān)鍵字:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc]init];
__block __weak Person *blockPerson = person;
MYBlock block = ^{
NSLog(@"%@",blockPerson);
};
block();
}
return 0;
}
查看底層代碼:
// __block 底層對(duì)象
struct __Block_byref_blockPerson_0 {
void *__isa;
__Block_byref_blockPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
//這里變成了弱引用,說明這里的引用情況取決于外部變量的修飾符
Person *__weak blockPerson;
};
// block 底層對(duì)象
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//這里還是強(qiáng)引用,說明 __weak 關(guān)鍵字并不會(huì)影響這里
__Block_byref_blockPerson_0 *blockPerson; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_blockPerson_0 *_blockPerson, int flags=0) : blockPerson(_blockPerson->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// main 函數(shù)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_blockPerson_0 *blockPerson = __cself->blockPerson; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_4ce55f_mi_0,(blockPerson->__forwarding->blockPerson));
}
查看底層代碼我們發(fā)現(xiàn),block內(nèi)部對(duì)于__Block_byref_blockPerson_0的引用沒有變化,但是__Block_byref_blockPerson_0中的blockPerson已經(jīng)從強(qiáng)引用變成了弱引用.如圖:

現(xiàn)在我們來驗(yàn)證一下
__Block_byref_blockPerson_0中的blockPerson對(duì)Person對(duì)象的強(qiáng)引用和弱引用兩種情況:
我們添加
__weak后再看看:
通過對(duì)比我們就驗(yàn)證了
__Block_byref_blockPerson_0中的blockPerson對(duì)Person對(duì)象的引用存在強(qiáng)引用和弱引用兩種情況,這兩種情況取決于Person對(duì)象的修飾符.這里需要注意一點(diǎn),
__Block_byref_blockPerson_0中的blockPerson對(duì)Person對(duì)象的強(qiáng)引用只有在ARC環(huán)境下才會(huì)retain,在MRC環(huán)境下只會(huì)弱引用.如圖:
我們演示一下,我們把環(huán)境切換為 MRC ,然后把 block copy 到堆上:

?大家思考一下,如果把
__block去掉,會(huì)怎么樣?如果把
__block去掉,blcok 就會(huì)對(duì) person 產(chǎn)生強(qiáng)引用,在 block 釋放之前,person 是不會(huì)釋放的,因?yàn)槿サ?code>__block后,就沒有__Block_byref_blockPerson_0這個(gè)中間層,blcok 會(huì)直接強(qiáng)引用 person對(duì)象,如圖:

