_我們先通過一個小場景,開始今天的主題.
有過開發(fā)經(jīng)驗的同學都知道,在block內(nèi)部是無法修改局部變量的,為什么不能修改呢?我們從底層一探究竟,我們把這段代碼轉成
C++
,查看一下底層:如圖所示,是無法修改的,那怎么樣才能在
block
內(nèi)部修改外部變量呢?有三種方法:1:使用
static
修飾age
2:把
age
變成全局變量3:使用
__block
修飾age
前兩種方法我們就不細說了,在Block如何捕獲外部變量一:基本數(shù)據(jù)類型中已經(jīng)說的很明白了,并且前兩種方法會一直保存在內(nèi)存中占用內(nèi)存.我們重點研究
__block
關鍵字.
一:__block的本質(zhì)
我們把使用__block
修飾的age
轉換為C++
代碼,對比一下:
可以發(fā)現(xiàn):使用
__block
修飾的age
底層被轉換成了一個__Block_byref_age_0
對象,我們重點研究一下這個對象.我們找到main
函數(shù)中聲明__block int age = 10
這句代碼的底層代碼:之前沒有使用
__block
修飾時,底層代碼就是int age = 10
,使用了__block
修飾后,會發(fā)現(xiàn)變化如此之大,我們對這句轉換后的代碼簡化一下:ok,現(xiàn)在我們來梳理一下使用__block
修飾的age
變量和block
的關系,為此,我截了一張 OC 代碼和轉換后的 C++ 代碼的對比圖,這樣能更清晰的展示他們之間的關系:
這張圖和清晰的展示了使用
__block
修飾的age
變量和block
的關系,我們思考一下在block
內(nèi)部是如何修改age
的值得,我們還是通過底層代碼查看:從截圖中可以看到,
block
內(nèi)部修改auto
變量,是先通過參數(shù)傳遞進來的block
找到age
結構體,然后通過age
結構體找到__forwarding
成員,通過之前的分析已經(jīng)知道__forwarding
存儲的指針指向age
結構體自己,所以本質(zhì)上還是通過age
結構體找到存儲auto
變量值得age
成員,然后修改成 20.
思考一:為什么蘋果要設計forwarding
這種多此一舉的方式呢?
因為當block從棧上拷貝到堆上后,__block變量
也會拷貝到堆上.這時就有兩份__block變量
,一份棧上的,一份堆上的.而棧上的__block變量
隨時可能銷毀,訪問時可能出現(xiàn)野指針情況,為了保證始終訪問同一份有效且安全的數(shù)據(jù),需要把棧上__block
的forwarding
指針指向堆上__block
地址.這樣就能保證即使訪問棧上的__block變量
也能獲取到堆上的變量值,如圖:
思考二:如果我們修改的外部變量是對象類型,它的底層是怎樣的呢?
通過對比我們發(fā)現(xiàn),對象類型也會包裝成一個結構體,并且這個結構體里面也會有一個成員存放
auto
變量的值.區(qū)別是對象類型的結構體里面多了copy
和dispose
兩個函數(shù),我們在Block如何捕獲外部變量二:對象類型里面已經(jīng)講過,因為對象類型會涉及到內(nèi)存管理問題.
思考三:如圖所示,我們在block
內(nèi)部給外部沒有使用__block
修飾的array
類型的auto
變量添加元素,會編譯成功嗎?
肯定會成功的,這里我們不要搞混淆了,我們給
array
添加元素,是使用這個地址,而不是修改這個地址,如果我們array = nil
這樣才會報錯.
思考四:通過上述分析,我們知道age
結構體中會有一個成員存儲auto
變量的值,所以現(xiàn)在就有兩個age
.一個是age 結構體
,另一個是存儲變量值得age成員
,那我們?nèi)绻蛴?code>age的地址,會是哪個age
的地址呢?
為了研究清楚這個問題,我們把底層的C++的
block
結構體挪到我們的OC代碼中,代碼如下:
typedef void(^MYBlock)(void);
//impl結構體
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//age結構體
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
//Desc結構體
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};
//block結構體
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;
}
這樣我們就實現(xiàn)了把Block
類型轉換為struct __main_block_impl_0
結構體,方便我們查看block
結構體中成員變量的地址.
我們在函數(shù)返回之前設置一個斷點,然后打印了一個地址
0x100405998
,我們從變量視圖中可以直接看到block
結構體中的age
結構體的地址是0x100405980
,可以看出打印的地址和age
結構體的地址并不相同.由此我們可以得出結論:打印的地址是age
結構體中的age
成員地址,并不是block
內(nèi)部捕獲的age
.ok,我們來驗證我們剛剛得出的結論.
- 驗證方法一:
我們在變量視圖中看到age
結構體的地址是0x100405980
,而我們知道,結構體的地址也是結構體第一個成員的地址,由此我們可以手動計算出結構體中age
成員的地址:
//age結構體
struct __Block_byref_age_0 {//0x100405980 age結構體地址
void *__isa;// 占8字節(jié) 0x100405980 第一個成員的地址和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
};
可以看到,我們計算處理的地址0x100405998
和打印出來的地址0x100405998
是相同的,驗證了我們剛才得出的結論.
- 驗證方法二:通過命令行打印地址:
我們通過LLDB命令p/x &(blockImpl->age->age)
打印出的age
地址和NSLog
輸出的地址是相同的,再一次驗證了我們剛才的結論.這張截圖中的地址和上一張圖中的地址不一樣,是因為這張圖是重新運行代碼截取的,變量的內(nèi)存地址已經(jīng)改變了,大家不用糾結這里.
?我們思考一下蘋果為什么這樣設計?因為蘋果要隱藏它內(nèi)部的實現(xiàn),我們在修改__block
修飾的age
的值時,從表面看會以為真的是在直接修改age
的值,如果不了解底層實現(xiàn)的話,根本就不知道被__block
修飾的age
已經(jīng)被包裝成了一個對象,而我們實際修改的是age
結構體中的age
成員的值.
二:__block的內(nèi)存管理
我們在block如何捕獲對象類型的文章中已經(jīng)知道,如果block訪問的是對象類型的變量,那么__main_block_desc_0
結構體中會增加copy,dispose
兩個函數(shù)指針,這兩個函數(shù)會根據(jù)變量的修飾符(__strong
,__weak
,__unretained
)進行相應的操作,形成強引用(retain
)或者弱引用或者是釋放引用的變量.現(xiàn)在我們來研究一下,使用__block
修飾的外部變量,在block內(nèi)部是如何管理的.
聲明一個__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;
}
然后我們轉成 C++ 代碼,看看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};
會發(fā)現(xiàn)同訪問了對象類型的變量一樣,__main_block_desc_0
結構體中同樣有copy,dispose
函數(shù),可能有人會有疑問,因為在block如何捕獲對象類型中我們說過,只有在訪問對象類型的auto
變量時才會生成copy dispose
.為什么訪問 __block int
時也會產(chǎn)生呢?
在上面我們說過,block
在訪問__block
修飾的變量時,其底層會被封裝成__Block_byref_age_0
類型,在這個類型存在一個void *__isa
成員,所以本質(zhì)上它就是一個對象類型.
Block
內(nèi)部是如何管理使用__block
修飾的變量的呢?
- 在棧上的block,不會對__block產(chǎn)生強引用
- 當block拷貝到堆上時
1:會調(diào)用block內(nèi)部的copy函數(shù)
2:copy函數(shù)內(nèi)部會調(diào)用__main_block_copy_0
函數(shù)
3:__main_block_copy_0
會調(diào)用_Block_object_assign
函數(shù)對__block變量形成強引用(return)
也就是說,一旦_Block_object_assign
調(diào)用,就會對block內(nèi)部的__block變量產(chǎn)生強引用
如圖:
思考一下:__block int age = 10
是存放在棧上的,而MYBlock myblock
是存放在堆上的,我們使用堆上的地址去指向椑恚空間肯定是不行的,那block的內(nèi)部是如何處理這種情況的呢?
結論就是,當棧上的blcok拷貝到堆上時,會把__block修飾的變量一同拷貝到堆上,如圖:
而當block從堆中移除時:
1:調(diào)用block內(nèi)部的dispose函數(shù)
2:dispose函數(shù)會調(diào)用
_Block_object_dispose
函數(shù)3:
_Block_object_dipose
函數(shù)會自動釋放引用的__block
變量(release)如圖:
思考一下,蘋果為什么會這么設計呢?其實這個也很好理解,因為使用__blcok
修飾的變量,底層其實被封裝成了對象,而我們要在block中使用這個對象,就肯定要對這個對象的引用負責.
既然 block 訪問對象類型的變量和訪問使用 __block 修飾的變量都會增加copy,dispose
函數(shù),那么他們之間有沒有區(qū)別呢?
他們之間的區(qū)別就是:
- 如果使用__block修飾的變量,block內(nèi)部直接對其強引用
- 如果是對象類型的變量,會根據(jù)變量的修飾符
__weak , __strong
來決定是否強引用
三:block訪問 __block 修飾的對象類型
到目前為止,我們講解了block訪問 基本數(shù)據(jù)類型 (int age) , __block 修飾的基本數(shù)據(jù)類型 (__block int age), 對象類型 NSObject *object
三種情況,下面我們分析一下第四種情況: __blcok 修飾的對象類型.
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)建一個Person
類,然后使用__block
修飾一個person
對象,在block 中訪問,查看底層代碼如下:
// __block 底層對象
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 中強引用 Person 對象
Person *__strong blockPerson;
};
// block 底層對象
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// block中強引用 __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
修飾的對象底層被封裝成了__Block_byref_blockPerson_0
類型的對象,__Block_byref_blockPerson_0
這個對象類型的結構體中有一個Person *__strong blockPerson
成員,強引用著我們在main
函數(shù)中創(chuàng)建的Person
對象,而__main_block_impl_0
中的blockPerson
又強引用著__Block_byref_blockPerson_0
對象,他們的關系如下圖:
我們稍作修改,在
blockPerson
添加__weak
關鍵字:
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 底層對象
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 底層對象
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//這里還是強引用,說明 __weak 關鍵字并不會影響這里
__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)部對于__Block_byref_blockPerson_0
的引用沒有變化,但是__Block_byref_blockPerson_0
中的blockPerson
已經(jīng)從強引用變成了弱引用.如圖:
現(xiàn)在我們來驗證一下
__Block_byref_blockPerson_0
中的blockPerson
對Person
對象的強引用和弱引用兩種情況:我們添加
__weak
后再看看:通過對比我們就驗證了
__Block_byref_blockPerson_0
中的blockPerson
對Person
對象的引用存在強引用和弱引用兩種情況,這兩種情況取決于Person
對象的修飾符.這里需要注意一點,
__Block_byref_blockPerson_0
中的blockPerson
對Person
對象的強引用只有在ARC環(huán)境下才會retain
,在MRC環(huán)境下只會弱引用.如圖:我們演示一下,我們把環(huán)境切換為 MRC ,然后把 block copy 到堆上:
?大家思考一下,如果把
__block
去掉,會怎么樣?如果把
__block
去掉,blcok 就會對 person 產(chǎn)生強引用,在 block 釋放之前,person 是不會釋放的,因為去掉__block
后,就沒有__Block_byref_blockPerson_0
這個中間層,blcok 會直接強引用 person
對象,如圖: