iOS Block原理

一 Block的實(shí)現(xiàn)

1. 在main函數(shù)中聲明、實(shí)現(xiàn)并調(diào)用一個(gè)block

block的聲明、實(shí)現(xiàn)和調(diào)用

2. 然后我們通過clang命令將main.m轉(zhuǎn)為c++文件

clang命令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m


block的c++實(shí)現(xiàn)

3. block的聲明和實(shí)現(xiàn)

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

在block的實(shí)現(xiàn)中使用了__main_block_impl_0函數(shù),并將__main_block_impl_0函數(shù)返回值的地址賦值給了我們聲明的block變量“block”

4.?__main_block_impl_0

__main_block_impl_0函數(shù)結(jié)構(gòu)

在__main_block_impl_0的結(jié)構(gòu)中我們看到一個(gè)同名的構(gòu)造函數(shù)(上圖紅框標(biāo)記的函數(shù))。這里我們可以看出,實(shí)現(xiàn)block時(shí),調(diào)用__main_block_impl_0函數(shù),并返回了一個(gè)__main_block_impl_0結(jié)構(gòu)體,并取了此結(jié)構(gòu)體的地址,所以變量“block”的實(shí)質(zhì)是一個(gè)__main_block_impl_0結(jié)構(gòu)體的地址。

5. __block_impl

__block_impl

可以看出__block_impl結(jié)構(gòu)與oc對(duì)象一樣,都有一個(gè)isa指針。其中FuncPtr字段保存的就是自定義邏輯代碼塊轉(zhuǎn)化后函數(shù)的指針。

5 __main_block_impl_0構(gòu)造函數(shù)的調(diào)用

此構(gòu)造函數(shù)有三個(gè)參數(shù),其中第三個(gè)參數(shù)在構(gòu)造函數(shù)聲明時(shí)直接賦值為0(int flags=0),所以我們不用考慮第三個(gè)參數(shù),主要分析另外兩個(gè)參數(shù),__main_block_func_0和&__main_block_desc_0_DATA

__main_block_impl_0構(gòu)造函數(shù)的調(diào)用

6. 參數(shù)__main_block_func_0

__main_block_func_0結(jié)構(gòu)

這里有個(gè)NSLog,我們分析這里就是我們實(shí)現(xiàn)block時(shí)的具體邏輯。我們?cè)趯?shí)現(xiàn)block時(shí),將自定義邏輯的代碼塊封裝成了__main_block_func_0函數(shù),并將__main_block_func_0函數(shù)的地址作為參數(shù)傳入了__main_block_impl_0構(gòu)造函數(shù),將__main_block_func_0存儲(chǔ)在__main_block_impl_0結(jié)構(gòu)體中。

7. 參數(shù)&__main_block_desc_0_DATA

__main_block_desc_0_DATA結(jié)構(gòu)

在上圖中我們看到__main_block_desc_0_DATA是對(duì)結(jié)構(gòu)體__main_block_desc_0初始化后的結(jié)果。在初始化時(shí)reserved被賦值為0,Block_size中存儲(chǔ)著__main_block_impl_0結(jié)構(gòu)體占用空間的大小。

8.?__main_block_impl_0構(gòu)造函數(shù)的實(shí)現(xiàn)

__main_block_impl_0構(gòu)造函數(shù)的實(shí)現(xiàn)

我們可以看到:

impl.isa存儲(chǔ)著表示block類型的內(nèi)容,這表明block實(shí)質(zhì)上是一個(gè)OC對(duì)象。

impl.Flags中存儲(chǔ)著0,是源碼中寫死的,暫時(shí)不討論此參數(shù)。

impl.FuncPtr中存儲(chǔ)的是block自定義邏輯代碼塊封裝的函數(shù)地址。

Desc中存儲(chǔ)著整個(gè)__main_block_impl_0結(jié)構(gòu)體所占空間的大小。

9. 一張圖總結(jié)一下block的實(shí)現(xiàn)過程

block實(shí)現(xiàn)過程

二 Block的調(diào)用

block調(diào)用

我們知道實(shí)現(xiàn)block時(shí),自定義邏輯的代碼塊是存儲(chǔ)在了FuncPtr中,所以調(diào)用過程實(shí)際就是找到FuncPtr并調(diào)用。

__main_block_impl_0結(jié)構(gòu)體內(nèi)存示意圖

我們從上面__main_block_impl_0結(jié)構(gòu)體內(nèi)存示意圖中看出,__main_block_impl_0結(jié)構(gòu)體的第一個(gè)成員是impl,它的地址與__main_block_impl_0的地址其實(shí)是一樣的,所以我們直接把__main_block_impl_0結(jié)構(gòu)體強(qiáng)轉(zhuǎn)為__block_impl結(jié)構(gòu)體,然后直接調(diào)用其中的FuncPtr。

這樣就完成了block的調(diào)用。

三 Block的變量捕獲

1. 捕獲局部自動(dòng)變量

捕獲局部自動(dòng)變量

上圖中變量a是一個(gè)局部自動(dòng)變量(省略了auto修飾詞)。從圖中我們可以分析出,block中訪問block外的局部值類型參數(shù)時(shí),是通過__main_block_impl_0結(jié)構(gòu)體緩存住參數(shù)值,然后再傳入__main_block_func_0函數(shù)中的。

這是一個(gè)簡(jiǎn)單的變量捕獲過程。

2. 捕獲多種值類型變量

捕獲多種值類型變量

這里聲明了4個(gè)變量:a,局部自動(dòng)變量;b,局部靜態(tài)變量;c,全局變量;d,全局靜態(tài)變量。

這四個(gè)變量被block捕獲的邏輯如下:

a:通過__main_block_impl_0結(jié)構(gòu)體緩存住a的值,然后通過__main_block_impl_0結(jié)構(gòu)體獲取到a的值

b:通過__main_block_impl_0結(jié)構(gòu)體緩存住b的地址,然后通過__main_block_impl_0結(jié)構(gòu)體獲取到b的地址,最終獲取到b的值

c:直接訪問

d:直接訪問

這樣我們就能解釋下圖這種情況了

實(shí)現(xiàn)block修改局部變量的值

我們?cè)趯?shí)現(xiàn)block之后修改了變量a、b、c、d的值,在打印時(shí)發(fā)現(xiàn),a的值并沒有改變。這是因?yàn)樵趯?shí)現(xiàn)block的值被賦值給了__main_block_impl_0結(jié)構(gòu)體內(nèi)的成員,這時(shí)block中使用的a已經(jīng)不是block外聲明的那個(gè)a了。

四 Block的類型

1. block的類型

block類型

圖中定義了6種情況的block:

1. 沒有使用外部變量 ????2. 使用了局部變量????????????3. 使用了靜態(tài)局部變量

4. 使用了全局變量????????5. 使用了全局靜態(tài)變量? ? ?6. 直接調(diào)用

結(jié)果可總結(jié)為:

block類型

這里我們看到block有三種類型,分別為NSGlobalBlock、NSMallocBlockNSStackBlock

2. block類型間的關(guān)系與區(qū)別

從上面6個(gè)block的分析中我們可以看出當(dāng)block中沒用通過__main_block_impl_0結(jié)構(gòu)體直接存儲(chǔ)變量的值時(shí),block的類型為NSGlobalBlock。

當(dāng)block中通過__main_block_impl_0結(jié)構(gòu)體重存儲(chǔ)了變量的值時(shí),block的類型為NSStackBlock。

但是這里有一個(gè)特例,block1也在__main_block_impl_0結(jié)構(gòu)體重存儲(chǔ)了變量的值,但是它的類型卻是NSMallocBlock。

這是因?yàn)樵贏RC模式下,如果有變量引用了block,則block會(huì)自動(dòng)被copy一次,將block從棧區(qū)拷貝到堆區(qū)。此時(shí)block的內(nèi)存控制由引用計(jì)數(shù)控制。在MRC過程中是沒有copy操作的。

由此我們可以知道,我們?cè)贏RC環(huán)境中生命block屬性時(shí),使用copy和strong關(guān)鍵詞修飾最終的效果都是一樣的,用strong修飾時(shí)block也會(huì)自動(dòng)copy。我們現(xiàn)在還是用copy修飾block屬性主要是延用MRC時(shí)代的寫法。

不同類型的block存儲(chǔ)位置

NSGlobalBlock:存儲(chǔ)在數(shù)據(jù)段(全局區(qū)),內(nèi)存在程序結(jié)束后由系統(tǒng)回收。

NSStackBlock:內(nèi)存由編譯器控制,過了作用域就會(huì)被回收。

NSMallocBlock:內(nèi)存根據(jù)引用計(jì)數(shù)控制。

3. block clang后的類型

block clang后的類型

我們可以看到,block0~block5對(duì)應(yīng)著__main_block_func_0~__main_block_func_5這個(gè)五個(gè)結(jié)構(gòu)體,下面我們分別看下這五個(gè)結(jié)構(gòu)體中的isa指針指向什么。

block clang后的類型

clang之后的block類型全部都是_NSConcreteStackBlock類型,與我們之前打印的結(jié)果不一致。我在網(wǎng)上找到了一些其他大神的分析,記錄在這里。

1. block0,block2,block3,block4并未使用外部變量,應(yīng)該是一個(gè)_NSConcreteGlobalBlock,而用clang顯示的卻是_NSConcreteStackBlock,唐巧的說法是:clang 改寫的具體實(shí)現(xiàn)方式和 LLVM 不太一樣。

2. 把編譯后的代碼copy到mm中后

((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();

此行會(huì)crash,因?yàn)?i>((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))指向的是impl的isa,也就是_NSConcreteStackBlock的指針。

需要改寫成

struct __main_block_impl_0 impl_0 = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);

((void (*)())(impl_0.impl.FuncPtr))();

才能執(zhí)行成功。

或許clang改寫的實(shí)現(xiàn)跟LLVM確實(shí)不一樣吧。

作者:WhiteZero鏈接:http://m.itdecent.cn/p/d96d27819679來源:簡(jiǎn)書著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

4. block類型的具體實(shí)現(xiàn)

我們?cè)赽lock捕獲局部自動(dòng)變量a時(shí)發(fā)現(xiàn),clang后的block實(shí)現(xiàn)自動(dòng)添加了一個(gè)變量a,這個(gè)a是從哪里來的?我們需要進(jìn)一步探尋block類型的底層結(jié)構(gòu)。

block中自動(dòng)多了一個(gè)參數(shù)a

源碼簡(jiǎn)要分析

我們?cè)谠创a中找到Block_private.h文件中找到Block_layout結(jié)構(gòu)體,此結(jié)構(gòu)體就是block的真實(shí)結(jié)構(gòu)。

最后面有個(gè)注釋,表示輸入的變量也會(huì)保存在此結(jié)構(gòu)體中,旁邊的內(nèi)存示意圖中variables就是保存輸入的變量的空間。

其中的isa就是指向block類型的(_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock)

block類型結(jié)構(gòu)

五 Block捕獲對(duì)象類型參數(shù)

1. 源碼分析

block源碼結(jié)構(gòu)

搞清楚了block的源碼結(jié)構(gòu)之后,我們?cè)賮砜纯碞SMallocBlock類型的block是怎么通過copy操作放進(jìn)堆區(qū)的。

1. Block_copy()

我們現(xiàn)在知道NSMallocBlock類型的block是對(duì)NSStackBlock類型的block進(jìn)行了一次copy后得到的,我們現(xiàn)在要來研究一下這個(gè)copy方法中做了什么操作。

我們?cè)赽lock源碼中的Block.h中看到了這個(gè)copy方法的定義,叫做Block_copy(...)

Block_copy(...)的定義

其本質(zhì)是_Block_copy()方法,這個(gè)方法的具體實(shí)現(xiàn)在runtime.cpp文件中

_Block_copy()方法分析
objc引用計(jì)數(shù)

如圖,objc的引用計(jì)數(shù)為3,這是因?yàn)?,new出來的內(nèi)存區(qū)域在賦值給objc時(shí)引用計(jì)數(shù)加一,在創(chuàng)建stack類型block時(shí),捕獲外部變量過程中會(huì)拷貝一份,這時(shí)引用計(jì)數(shù)加一,在將block賦值給變量“block”時(shí)又執(zhí)行了一次copy操作,這時(shí)objc的引用計(jì)數(shù)又加了一,所以此時(shí)objc的引用計(jì)數(shù)是3。

3. Block_descriptor_2

我們從上邊_Block_copy()方法分析中看到,在copy過程中調(diào)用了_Block_call_copy_helper函數(shù),在這個(gè)函數(shù)中判斷了是否有Block_descriptor_2結(jié)構(gòu)體,如果有的話我們將調(diào)用其中的copy函數(shù)。

我們先來看下Block_descriptor_2結(jié)構(gòu)體是什么樣的。

Block_descriptor_2結(jié)構(gòu)

其中copy和dispose都是一個(gè)函數(shù)的指針,而這兩個(gè)函數(shù)具體在哪里實(shí)現(xiàn)的呢?

我們構(gòu)造一個(gè)block,不能block中捕獲一個(gè)OC對(duì)象的變量,clang之后就可以看到這兩個(gè)函數(shù)。

NSMallocBlock類型block結(jié)構(gòu)

我們?cè)赺_main_block_desc_0_DATA結(jié)構(gòu)體中看到,多了兩個(gè)字段,而且在初始化時(shí)將__main_block_copy_0和__main_block_dispose_0這兩個(gè)函數(shù)的地址傳了進(jìn)去。

我們接下來分析這兩個(gè)函數(shù)。

4. __main_block_copy_0

__main_block_copy_0函數(shù)的本質(zhì)是_Block_object_assign函數(shù),我們分析一下_Block_object_assign函數(shù)。

_Block_object_assign()函數(shù)分析

5.?__main_block_dispose_0

__main_block_copy_0函數(shù)的本質(zhì)是_Block_object_dispose函數(shù),我們分析一下_Block_object_dispose()函數(shù)。

_Block_object_dispose()函數(shù)

6. 總結(jié)一下

(1)在實(shí)現(xiàn)block時(shí),如果block引用了外部局部變量,且block被引用時(shí),block的類型被設(shè)置為NSMallocBlock類型

(2)對(duì)象的傳遞與值類型一樣也是在__main_block_impl_0結(jié)構(gòu)體中新增一個(gè)字段保存

(2)在block中捕獲OC對(duì)象的參數(shù)后看到,系統(tǒng)自動(dòng)構(gòu)造出了__main_block_copy_0和__main_block_dispose_0兩個(gè)函數(shù)

(3)__main_block_copy_0和__main_block_dispose_0這兩個(gè)函數(shù)保存在__main_block_desc_0_DATA結(jié)構(gòu)體中

(4)NSMallocBlock是NSStackBlock copy得來的,在copy的過程中通過__main_block_copy_0和__main_block_dispose_0這兩個(gè)方法對(duì)捕獲的對(duì)象進(jìn)行內(nèi)存管理。

六 __block修飾符

1. __block修飾符源碼分析

當(dāng)我們希望在block中修改變量的影響范圍擴(kuò)展到block之外,我們需要在聲明變量時(shí)使用__block修飾符,那這個(gè)修飾符具體是怎么樣實(shí)現(xiàn)這一功能的?我們先clang一下,看下具體實(shí)現(xiàn)是怎樣的。

__block底層實(shí)現(xiàn)

可以看到,用__block修飾的變量被構(gòu)造成了__Block_byref_objc_0類型的結(jié)構(gòu)體,在構(gòu)造函數(shù)__main_block_impl_0初始化時(shí),對(duì)象objc被賦值為__Block_byref_objc_0結(jié)構(gòu)體的__forwarding字段。

我們先來看下__Block_byref_objc_0結(jié)構(gòu)體的結(jié)構(gòu)。

__Block_byref_objc_0結(jié)構(gòu)

這里看到前面賦值給objc字段的__forwarding也是一個(gè)__Block_byref_objc_0結(jié)構(gòu)體,還是看不出來__block是如何實(shí)現(xiàn)的。

2. __block修飾符原理

我們先將main.h文件修改到mrc模式,修改方式如下

mrc模式

修改到mrc模式之后實(shí)現(xiàn)下面的代碼

__bllock修飾原理

打印結(jié)果如下

__block修飾原理打印結(jié)果

在copy的過程中,有__block修飾的OC對(duì)象,會(huì)被構(gòu)造在__Block_byref_object_0結(jié)構(gòu)體的__forwarding字段中。

在棧block中,__forwarding指向的是自己(棧中的__Block_byref_object_0結(jié)構(gòu)體),棧block經(jīng)歷了copy操作之后,將__Block_byref_object_0結(jié)構(gòu)體拷貝到了堆中。

堆中__Block_byref_object_0結(jié)構(gòu)體的__forwarding指向的也是自己(堆中的__Block_byref_object_0結(jié)構(gòu)體),

但是此時(shí)棧中__Block_byref_object_0結(jié)構(gòu)體的__forwarding指向的不是自己了(指向了堆中的__Block_byref_object_0結(jié)構(gòu)體)。

我們看看此時(shí)block中使用objc的具體實(shí)現(xiàn)

使用__block修飾的對(duì)象

在使用objc時(shí)通過__forwarding重新指向了對(duì)象的存儲(chǔ)位置,這樣copy之后無論是在棧block中還是堆block中訪問的都是同一個(gè)objc對(duì)象了。

最后我們還可以自己嘗試一下,在block copy之后在block外直接打印objc,此時(shí)objc指針位置也與其他block中的objc同步了。

__forwarding的操作是在__main_block_copy_0函數(shù)當(dāng)中的__Block_byref_copy()函數(shù)中實(shí)現(xiàn)的。

參考文獻(xiàn)

iOS底層原理探索— block的本質(zhì)(一)

史上最詳細(xì)的Block源碼剖析

Block 簽名信息的使用

公眾號(hào)

關(guān)注公眾號(hào),回復(fù)26H49獲取block源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容