目錄
一缤苫、block執(zhí)行體里無法修改外界的普通局部變量,可以用__block
修飾符修飾一下
二优俘、__block
修飾符的底層實現(xiàn)和__block變量的本質(zhì)
三玫膀、__block變量的內(nèi)存管理
?1、系統(tǒng)把棧blockcopy
到堆區(qū)時偷办,也會復(fù)制一份__block變量到堆區(qū)艰额,并讓堆block持有它
?2、并且讓棧__block變量的forwarding
指針指向堆上面的__block變量
由于捕獲變量椒涯,會導(dǎo)致無法修改外界的普通局部變量柄沮。
一、block執(zhí)行體里無法修改外界的普通局部變量废岂,可以用__block
修飾符修飾一下
上一篇我們說過了祖搓,“block會捕獲普通局部變量,即如果block的執(zhí)行體里使用了外界的局部變量湖苞,那么block內(nèi)部會生成一個與普通局部變量同名的成員變量拯欧,并且普通局部變量還會通過值傳遞的方式把值傳遞給這個成員變量,那么接下來block執(zhí)行體里使用的這個變量就不是外界的普通局部變量了财骨,而是block體內(nèi)的成員變量”镐作。所以block執(zhí)行體里無法修改外界的普通局部變量藏姐,就是因為block會捕獲這個普通局部變量,然后block執(zhí)行體里使用的這個變量就不是外界的普通局部變量了该贾,而是block體內(nèi)的成員變量羔杨,這還怎么改人家嘛。但是系統(tǒng)對我們屏蔽了block捕獲變量的機(jī)制杨蛋,這就導(dǎo)致在我們開發(fā)者眼里修改這個“外界的普通局部變量”修改的就是外界的普通局部變量啊兜材,可實際上修改的卻是block體內(nèi)的成員變量,為了避免造成歧義逞力,系統(tǒng)索性直接讓編譯器報錯了曙寡。
但實際開發(fā)中,我們可能確實有這樣的需求啊寇荧,怎么辦呢举庶?辦法其實有很多,比如我們可以把這個普通局部變量搞成靜態(tài)局部變量砚亭,因為block捕獲靜態(tài)局部變量是指針傳遞灯变,所以那個函數(shù)可以通過block內(nèi)部的指針順利修改外界的變量。我們還可以把這個局部變量搞成全局變量捅膘、靜態(tài)全局變量添祸,這當(dāng)然能修改了,全局變量哪里都能訪問得到寻仗,block的實現(xiàn)部分自然也能訪問得到刃泌。美則美矣,了則未了署尤,因為我們的需求就是臨時定義一個局部變量用用耙替,用完系統(tǒng)就自動銷毀它,不想像上面那樣搞一個靜態(tài)全局區(qū)的變量一直存在內(nèi)存中曹体,那還能怎么辦呢俗扇?可以用__block
修飾符修飾一下。
現(xiàn)在我們就用__block
修飾一下age
變量箕别,發(fā)現(xiàn)block實現(xiàn)部分確實能修改這個局部變量了铜幽。
typedef void (^INEBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 25;
INEBlock block = ^{
age = 26;
NSLog(@"%d", age); // 26
};
block();
}
return 0;
}
那為什么用__block
修飾符修飾之后,就能修改了呢串稀?這就需要看看__block
修飾符的底層實現(xiàn)除抛、__block變量的本質(zhì)了,內(nèi)容比較多母截,我們專門開一個部分來看看到忽。
二、__block
修飾符的底層實現(xiàn)和__block變量的本質(zhì)
我們來看下第一部分兩段代碼——普通局部變量和__block變量——的C/C++實現(xiàn)清寇。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 創(chuàng)建局部變量
int age = 25;
// 創(chuàng)建block
INEBlock block = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age,
570425344
);
// 調(diào)用block
block->impl.FuncPtr(block);
}
return 0;
}
// block的本質(zhì)
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age; // 捕獲的是局部變量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 創(chuàng)建__block變量
__Block_byref_age_0 age = {
0, // isa指針喘漏,值為0
&age, // __forwarding指針护蝶,把a(bǔ)ge這個__block變量自己的地址給傳進(jìn)去了,所以說這個指針指向它自己
0,
sizeof(__Block_byref_age_0),
25 // age成員變量陷遮,其實就是外界那個局部變量滓走,值為25
};
// 創(chuàng)建block
INEBlock block = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
&age,
570425344
);
// 調(diào)用block
block->impl.FuncPtr(block);
}
return 0;
}
// __block變量的本質(zhì)
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
// block的本質(zhì)
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // 捕獲的是__block變量的地址
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// block的實現(xiàn)部分(即生成的那個函數(shù))
void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // __cself可以看做是block(其實是block的地址),這里取出block捕獲的__block變量的地址
(age->__forwarding->age) = 26; // __forwarding指針不是指向__block變量自己嘛帽馋,所以這里就是相當(dāng)于 (age->age) = 26,即獲取__block變量的局部變量并修改
NSLog(age->__forwarding->age);
}
可見:用__block
修飾符修飾后的局部變量比吭,已經(jīng)不再是一個普通的局部變量了绽族,而是一個__block變量。所謂__block變量就是一個OC對象嘛衩藤,因為它里面有isa
指針喔吧慢,這個對象內(nèi)部會有一個成員變量來存儲之前那個普通局部變量的值,所以這就跟一個person
對象里有一個age
屬性沒啥區(qū)別赏表,所以block捕獲__block變量就像上一篇捕獲指針變量一樣是捕獲地址了检诗,此外我們需要知道它內(nèi)部還有一個__forwarding
指針指向它自己,這在后面說內(nèi)存管理時會用到瓢剿。
所以我們就能在block的執(zhí)行體里(即block對應(yīng)的函數(shù)體里)通過“block --> block捕獲的__block變量地址 --> __block變量 --> __block變量內(nèi)部的局部變量”這條路線來修改“外界的局部變量”的值了(其實根本就沒有“外界的局部變量”這個東西逢慌,只是我們開發(fā)者看起來像是這樣),類似于通過指針修改靜態(tài)局部變量那種方式间狂。
回到需求的出發(fā)點:我們就是想搞一個局部變量攻泼,并不想把局部變量搞成一個全局的東西。新生成的__block變量就是存儲在棧區(qū)的鉴象,該什么時候銷毀還是什么時候銷毀忙菠,只不過是對原來的局部變量做了一下包裝,并沒有改變局部變量的存儲域纺弊,它還是一個局部變量牛欢,這就完美地實現(xiàn)了我們的需求。
上面僅僅是演示了
__block
修飾基本數(shù)據(jù)類型的局部變量淆游,那么當(dāng)__block
修飾局部對象類型的指針變量時呢傍睹?全都一樣的,只不過多了一點稽犁,__block變量會根據(jù)它修飾的強(qiáng)指針還是弱指針來決定要不要持有該指針指向的對象焰望。
三、__block變量的內(nèi)存管理
1已亥、系統(tǒng)把棧blockcopy
到堆區(qū)時熊赖,也會把棧block內(nèi)部使用的__block變量復(fù)制一份到堆區(qū),并讓堆block持有它
新生成的__block變量是在棧區(qū)的虑椎,新創(chuàng)建的block也是在棧區(qū)(因為系統(tǒng)就是會為特定的block開辟棧區(qū)震鹉,這里我們也不談全局block)俱笛,此時即便block內(nèi)部使用了__block變量,block也不會持有__block變量(上面說了__block變量是個對象哦)传趾,因為棧block永遠(yuǎn)不會持有別人迎膜。
可是接下來當(dāng)我們把棧blockcopy
到堆區(qū)時(ARC下系統(tǒng)會自動做這個操作),系統(tǒng)也會把棧block內(nèi)部使用的__block變量復(fù)制一份到堆區(qū)(為什么浆兰?因為block都去堆區(qū)了磕仅,你__block變量還在棧區(qū),說不準(zhǔn)什么時候就被銷毀了簸呈,那block還訪問個啥)榕订,并調(diào)用block內(nèi)部的copy
函數(shù)讓copy
出來的堆block強(qiáng)引用這個__block變量(注意這個和__block變量是強(qiáng)指針還是弱指針沒有關(guān)系,這里系統(tǒng)設(shè)計就是要強(qiáng)引用它蜕便,__block變量雖然是個OC對象劫恒,但它和普通的OC對象又不太一樣,沒有強(qiáng)指針轿腺、弱指針一說)两嘴。
而且如果有多個block使用了同一個__block變量,則只會在
copy
第一個block到堆區(qū)時復(fù)制一份__block變量到堆區(qū)族壳,后面的blockcopy
到堆區(qū)時就不會再復(fù)制__block變量到堆區(qū)了憔辫,因為所有的__block變量都是同一個嘛,只需要增加它的引用計數(shù)就可以了决侈。
而當(dāng)block銷毀時螺垢,系統(tǒng)就會調(diào)用block內(nèi)部的dispose
方法來釋放對__block變量的強(qiáng)引用,也就是說__block變量的生命周期取決于有多少個block內(nèi)部訪問了它赖歌,直到所有訪問它的block都釋放了枉圃,他就會被釋放。
2庐冯、并且讓棧__block變量的forwarding
指針指向堆上面的__block變量
當(dāng)我們把棧block和它內(nèi)部使用的__block變量都復(fù)制到堆區(qū)后孽亲,棧block銷毀后大家使用的就是堆上的這份block和__block變量了,但是棧block銷毀前呢展父?我們得保證棧block銷毀前返劲,它內(nèi)部對__block變量數(shù)據(jù)的寫入和讀取都指向堆區(qū)的那個__block變量,只有這樣當(dāng)棧block銷毀后栖茉,我們開始使用堆block訪問__block變量時篮绿,數(shù)據(jù)才是最新的,否則堆__block變量的數(shù)據(jù)就又可能滯后于棧__block變量的數(shù)據(jù)吕漂,這種現(xiàn)象是不應(yīng)該發(fā)生的亲配,所以系統(tǒng)把棧blockcopy
到堆區(qū)時,會讓棧__block變量的forwarding
指針指向堆上面的__block變量,當(dāng)然堆__block變量的forwarding
指針還是指向它自己吼虎。