讓我們一起研究一下__block實(shí)現(xiàn)原理。
1.普通非對(duì)象的變量
先來(lái)看看普通變量的情況。

把上述代碼用clang轉(zhuǎn)換成源碼。

從源碼我們能發(fā)現(xiàn),帶有 __block的變量也被轉(zhuǎn)化成了一個(gè)結(jié)構(gòu)體__Block_byref_i_0,這個(gè)結(jié)構(gòu)體有5個(gè)成員變量。第一個(gè)是isa指針,第二個(gè)是指向自身類(lèi)型的__forwarding指針,第三個(gè)是一個(gè)標(biāo)記flag,第四個(gè)是它的大小,第五個(gè)是變量值,名字和變量名同名。

源碼中是這樣初始化的。__forwarding指針初始化傳遞的是自己的地址。然而這里__forwarding指針真的永遠(yuǎn)指向自己么?我們來(lái)做一個(gè)實(shí)驗(yàn)。

我們把Block拷貝到了堆上,這個(gè)時(shí)候打印出來(lái)的2個(gè)i變量的地址就不同了。

地址不同就可以很明顯的說(shuō)明__forwarding指針并沒(méi)有指向之前的自己了。那__forwarding指針現(xiàn)在指向到哪里了呢?
Block里面的__block的地址和Block的地址就相差1052。我們可以很大膽的猜想,__block現(xiàn)在也在堆上了。
出現(xiàn)這個(gè)不同的原因在于這里把Block拷貝到了堆上。
由第二章里面詳細(xì)分析的,堆上的Block會(huì)持有對(duì)象。我們把Block通過(guò)copy到了堆上,堆上也會(huì)重新復(fù)制一份Block,并且該Block也會(huì)繼續(xù)持有該__block。當(dāng)Block釋放的時(shí)候,__block沒(méi)有被任何對(duì)象引用,也會(huì)被釋放銷(xiāo)毀。
__forwarding指針這里的作用就是針對(duì)堆的Block,把原來(lái)__forwarding指針指向自己,換成指向_NSConcreteMallocBlock上復(fù)制之后的__block自己。然后堆上的變量的__forwarding再指向自己。這樣不管__block怎么復(fù)制到堆上,還是在棧上,都可以通過(guò)(i->__forwarding->i)來(lái)訪問(wèn)到變量值。

所以在__main_block_func_0函數(shù)里面就是寫(xiě)的(i->__forwarding->i)。這里還有一個(gè)需要注意的地方。還是從例子說(shuō)起:

結(jié)果和之前copy的例子完全不同。

Block在捕獲住__block變量之后,并不會(huì)復(fù)制到堆上,所以地址也一直都在棧上。這與ARC環(huán)境下的不一樣。
ARC環(huán)境下,不管有沒(méi)有copy,__block都會(huì)變copy到堆上,Block也是__NSMallocBlock。
感謝@酷酷的哀殿 指出錯(cuò)誤,感謝@bestswifter 指點(diǎn)。上述說(shuō)法有點(diǎn)不妥,詳細(xì)見(jiàn)文章末尾更新。
ARC環(huán)境下,一旦Block賦值就會(huì)觸發(fā)copy,__block就會(huì)copy到堆上,Block也是__NSMallocBlock。ARC環(huán)境下也是存在__NSStackBlock的時(shí)候,這種情況下,__block就在棧上。
MRC環(huán)境下,只有copy,__block才會(huì)被復(fù)制到堆上,否則,__block一直都在棧上,block也只是__NSStackBlock,這個(gè)時(shí)候__forwarding指針就只指向自己了。

至此,__block的實(shí)現(xiàn)原理也已經(jīng)明了。
2.對(duì)象的變量
還是先舉一個(gè)例子:

輸出

我們把上面的代碼轉(zhuǎn)換成源碼研究一下:

首先需要說(shuō)明的一點(diǎn)是對(duì)象在OC中,默認(rèn)聲明自帶__strong所有權(quán)修飾符的,所以main開(kāi)頭我們聲明的

等價(jià)

在轉(zhuǎn)換出來(lái)的源碼中,我們也可以看到,Block捕獲了__block,并且強(qiáng)引用了,因?yàn)樵赺_Block_byref_block_obj_0結(jié)構(gòu)體中,有一個(gè)變量是id block_obj,這個(gè)默認(rèn)也是帶__strong所有權(quán)修飾符的。
根據(jù)打印出來(lái)的結(jié)果來(lái)看,ARC環(huán)境下,Block捕獲外部對(duì)象變量,是都會(huì)copy一份的,地址都不同。只不過(guò)帶有__block修飾符的變量會(huì)被捕獲到Block內(nèi)部持有。
我們?cè)賮?lái)看看MRC環(huán)境下的情況,還是將上述代碼的例子運(yùn)行在MRC中。
輸出:

這個(gè)時(shí)候block在棧上,__NSStackBlock__,可以打印出來(lái)retainCount值都是1。當(dāng)把這個(gè)block copy一下,就變成__NSMallocBlock__,對(duì)象的retainCount值就會(huì)變成2了。
總結(jié):
在MRC環(huán)境下,__block根本不會(huì)對(duì)指針?biāo)赶虻膶?duì)象執(zhí)行copy操作,而只是把指針進(jìn)行的復(fù)制。
而在ARC環(huán)境下,對(duì)于聲明為_(kāi)_block的外部對(duì)象,在block內(nèi)部會(huì)進(jìn)行retain,以至于在block環(huán)境內(nèi)能安全的引用外部對(duì)象,所以才會(huì)產(chǎn)生循環(huán)引用的問(wèn)題!
關(guān)于Block捕獲外部變量有很多用途,用途也很廣,只有弄清了捕獲變量和持有的變量的概念以后,之后才能清楚的解決Block循環(huán)引用的問(wèn)題。
再次回到文章開(kāi)頭,5種變量,自動(dòng)變量,函數(shù)參數(shù) ,靜態(tài)變量,靜態(tài)全局變量,全局變量,如果嚴(yán)格的來(lái)說(shuō),捕獲是必須在Block結(jié)構(gòu)體__main_block_impl_0里面有成員變量的話,Block能捕獲的變量就只有帶有自動(dòng)變量和靜態(tài)變量了。捕獲進(jìn)Block的對(duì)象會(huì)被Block持有。
對(duì)于非對(duì)象的變量來(lái)說(shuō),
自動(dòng)變量的值,被copy進(jìn)了Block,不帶__block的自動(dòng)變量只能在里面被訪問(wèn),并不能改變值。
帶__block的自動(dòng)變量 和 靜態(tài)變量 就是直接地址訪問(wèn)。所以在Block里面可以直接改變變量的值。
而剩下的靜態(tài)全局變量,全局變量,函數(shù)參數(shù),也是可以在直接在Block中改變變量值的,但是他們并沒(méi)有變成Block結(jié)構(gòu)體__main_block_impl_0的成員變量,因?yàn)樗麄兊淖饔糜虼?,所以可以直接更改他們的值?/p>
值得注意的是,靜態(tài)全局變量,全局變量,函數(shù)參數(shù)他們并不會(huì)被Block持有,也就是說(shuō)不會(huì)增加retainCount值。
對(duì)于對(duì)象來(lái)說(shuō),
在MRC環(huán)境下,__block根本不會(huì)對(duì)指針?biāo)赶虻膶?duì)象執(zhí)行copy操作,而只是把指針進(jìn)行的復(fù)制。
而在ARC環(huán)境下,對(duì)于聲明為_(kāi)_block的外部對(duì)象,在block內(nèi)部會(huì)進(jìn)行retain,以至于在block環(huán)境內(nèi)能安全的引用外部對(duì)象。