Block究竟是什么,我們先從c++代碼開始
從一個(gè)最簡(jiǎn)單的block結(jié)構(gòu)開始

clang -rewrite-objc main.m -o main.cpp && open main.cpp


為了方便閱讀 我們簡(jiǎn)化一下代碼

為了方便進(jìn)一步閱讀,這里對(duì)其中的命名做了簡(jiǎn)化,參考下面的簡(jiǎn)單流程

-
結(jié)合clang編譯中間c++代碼,通過block的創(chuàng)建,結(jié)合上圖, 腦子里先勾勒一個(gè)sketch
-
創(chuàng)建兩層結(jié)構(gòu)
BlockCreate 結(jié)構(gòu)
Block結(jié)構(gòu),屬于BlockCreate的成員
通過BlockCreate構(gòu)造傳參,實(shí)例化BlockCreate成員Block::block
最終返回的是一個(gè) BlockCreate結(jié)構(gòu)指針
-
通過BlockCreate結(jié)構(gòu)首地址,我們可以拿到成員Block::block, BlockCreate首地址與 成員block首地址是一樣的,因?yàn)閎lock位于 BlockCreate內(nèi)存空間的起始處
既然可以拿到首地址(成員block地址),那么同樣可以通過內(nèi)存偏移拿到成員Desc的地址
通過拿到成員Block::block地址,就可以調(diào)用Block::block的成員方法FuncPtr了, 而FuncPtr恰恰是通過 BlockCreate構(gòu)造實(shí)例化Block::block成員時(shí) 賦值的fun函數(shù)入口地址
-
- 一定要了解這個(gè)兩層結(jié)構(gòu),雖然不是真正意義源碼,但是對(duì)后面我們分析源碼很有幫助
前面的例子沒有使用變量,我們可以通過前面的方式再操作一次,對(duì)比下區(qū)別


當(dāng)自定義的結(jié)構(gòu)體內(nèi)部訪問外部的一個(gè)局部變量時(shí)
你會(huì)發(fā)現(xiàn)clang生成的c++代碼發(fā)生了變化 對(duì)照上面的那個(gè)實(shí)例化流程
BlockCreate 結(jié)構(gòu)體內(nèi)多了一個(gè)成員 int a
BlockCreate構(gòu)造 也多了一個(gè)傳參
func函數(shù)內(nèi)部訪問變量 a通過 func(BlockCreate self) 的 BlockCreate::self 來(lái)獲取拷貝
-
你會(huì)發(fā)現(xiàn)有3處存在變量a
main函數(shù)內(nèi)的局部變量a
BlockCrete 結(jié)構(gòu)體內(nèi)的成員變量a
func方法內(nèi)部的局部變量a
其實(shí)這3個(gè)變量a分別是3個(gè)不同的變量了
把局部變量a改為static修飾,繼續(xù)clang c++查看


用static修飾變量a,不一樣了
BlockCreate構(gòu)造傳參,此時(shí)傳遞的是 a的地址,而BlockCreate成員 a也變成了 指針, func內(nèi)部的局部變量a 也變成了 指針,func內(nèi)部的a是通過 BlockCreate::*self 的指針a 賦值 給func內(nèi)部的局部變量 指針a
所以static修飾a后,func內(nèi)部訪問的a其實(shí)還是 main函數(shù)內(nèi)部的 指針a
把局部變量a改為 __block修飾,繼續(xù)clang c++查看


希望你不會(huì)覺得懵,這次復(fù)雜了些
出現(xiàn)了一個(gè)結(jié)構(gòu) __Block_byref_a_0
BlockCreate 成員Desc的結(jié)構(gòu)內(nèi)部多了兩個(gè) 函數(shù) copy & dispose
這里簡(jiǎn)單解釋下
-
普通的局部變量a 變成了一個(gè)結(jié)構(gòu) __Block_byref_a_0, a是這個(gè)結(jié)構(gòu)的成員
成員 void *__isa
成員 __block_byref_a_0 *__forwarding;
成員 int __flags;
成員 int __size
成員 int a
__isa 從前面的截圖可知。是一個(gè)_NSConcreteStackBlock 也就是棧block指針
在main里聲明的__block修飾的局部變量, 地址賦值給了 __forwarding, 值賦給了 Block_byref結(jié)構(gòu)里的成員a,注意這個(gè)設(shè)定, 雖然成員也叫a,只是起到一個(gè)接收值的作用,關(guān)鍵在于__forwarding 拿到了原來(lái)的a的指針
先看下__block修飾的a究竟是怎么訪問的
image.png
__forwarding 類型 __Block_byref_a_0 *,類似于鏈表節(jié)點(diǎn),所以也是一個(gè)指向 __Block_byref_a_0 結(jié)構(gòu)的指針 至于有什么用,暫存疑,后面源碼接著分析
對(duì)比著看,其實(shí)很明顯,不難理解
image.png
block源碼 - libclosure-79 查看
源碼入口該怎么查看呢,我們先通過匯編看下

既然retainBlock,說明block開辟了空間,進(jìn)入查看

繼續(xù)跳轉(zhuǎn) br x16

目前找到了_Block_copy這樣一個(gè)符號(hào),然后進(jìn)入源碼查看

你會(huì)看到一個(gè)結(jié)構(gòu)Block_layout

Block_layout 就是前面通過clang c++代碼 分析出的 兩層結(jié)構(gòu)BlockCreate成員 Block::block
__block 修飾變量 測(cè)試代碼放進(jìn) block源碼進(jìn)行調(diào)試

這段代碼是在block源碼中測(cè)試的

這其實(shí)就是依照Block_layout 棧上的空間結(jié)構(gòu),在堆區(qū)創(chuàng)建了一個(gè)Block_layout結(jié)構(gòu)
同時(shí) 新開辟的Block_layout結(jié)構(gòu)->invoke 從原來(lái)?xiàng)I螧lock_layout->invoke拷貝過來(lái)

既然是堆上開辟空間創(chuàng)建的Block_layout結(jié)構(gòu),自然isa 指向 _NSConcreteMallocBlock (堆block)
block分析源碼遇到問題
現(xiàn)在還有兩塊沒探索到源碼,就是 前面通過clang 編譯生成的c++代碼中__Block_byref_a_0這樣的結(jié)構(gòu),還有一塊是BlockCreate構(gòu)造邏輯部分
那么接下來(lái)該何去何從?
我選擇最原始的方式 匯編 + 下符號(hào)斷點(diǎn) + 結(jié)合clang c++代碼分析

先把代碼斷到此處,防止dyld其他流程干擾

下符號(hào)斷點(diǎn) 同時(shí)把前面分析過的 _Block_copy 符號(hào)也下下來(lái),為了方便分析流程
跟著調(diào)試 進(jìn)入 _Block_object_dispose:

回到之前clang編譯出的c++代碼看下

既然下到了符號(hào)_Block_object_dispose 那么同樣也把符號(hào) _Block_object_copy下下來(lái)繼續(xù)調(diào)試
沒有的話 就試試 _Block_object_assign, 之所以沒有找到 _Block_object_copy符號(hào),是因?yàn)槟鞘怯删幾g器決定的
成功斷點(diǎn)符號(hào) _Block_object_assign

找到頭緒,自然我們又回到了源碼

-
看下源碼注釋
When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point to do the assignment.
當(dāng)Blocks(可以理解為前面的有成員func的那個(gè)結(jié)構(gòu)) 或者 Block_byref持有對(duì)象時(shí)候,這個(gè)入口就會(huì)被觸發(fā) 執(zhí)行賦值操作

-
__block int a = 10 類型為 BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK or BLOCK_FIELD_IS_BYREF
執(zhí)行 _Block_byref_copy()
_Block_byref_copy
在分析_Block_byref_copy流程之前,我們需要了解下Block_byref 是什么

從前面clang編譯拿到的c++代碼,可以看到,Block_byref 是對(duì)常規(guī)變量的封裝,封裝結(jié)構(gòu)里還多了isa,__forwarding成員

源碼中還存在 Block_byref_2 Block_byref_3 兩個(gè)結(jié)構(gòu),暫且不表,后面會(huì)繼續(xù)說明
我們可以做個(gè)假設(shè),目前我們測(cè)試的實(shí)例 是block引用外部 __block修飾的變量,我們也是這么用的,既然block內(nèi)部訪問外部變量,那么也會(huì)對(duì)于這個(gè)變量的引用計(jì)數(shù)產(chǎn)生影響 flags就是存儲(chǔ)引用計(jì)數(shù)的
_Block_byref_copy翻譯

如果源byref結(jié)構(gòu)已經(jīng)在heap上,則不需要執(zhí)行拷貝,引用計(jì)數(shù)+1

中間有一段內(nèi)存偏移的代碼,還沒解析,繼續(xù)

byref_keep byref_destroy 究竟實(shí)現(xiàn)了什么功能
因?yàn)槲覀冇玫某R?guī)變量a測(cè)試 我們換成object看下
將變量a換為object測(cè)試

clang c++代碼


從源碼得知



131有什么意義

兩個(gè)參數(shù) + 40 什么意思

按照編譯的邏輯,byref_keep 就是 object類型的對(duì)象的 拷貝
但是運(yùn)行時(shí)會(huì)做修正 流程有差別
同樣 byref_destroy:

以上為 Block_byref 邏輯,再通過clang得到的c++ 看下 Block_layout 的處理


再確認(rèn)下 __block修飾的 object對(duì)象,在block體里 究竟是如何訪問的

總結(jié)
__block 修飾變量之后,編譯器會(huì)在棧上構(gòu)建一個(gè) 棧Block_byref(包含變量指針)
-
定義block,可以理解為編譯器生成一個(gè)中間結(jié)構(gòu)BlockCreate(這個(gè)名字是特意起的,知道是個(gè)結(jié)構(gòu),為了便于理解,你可以這么理解)
- 同時(shí)編譯器會(huì)在棧上初始化構(gòu)建一個(gè) 棧Block_layout(包含func成員)
-
執(zhí)行BlockCreate構(gòu)造方法
通過Block_layout首地址偏移 得到 Block_copy函數(shù)地址, 執(zhí)行Block_copy,把 棧Block_byref 拷貝 到堆Block_byref
構(gòu)造參數(shù) 棧Block_byref,通過Block_byref首地址偏移 得到 Block_byref_2(包含_Block_byref_copy 即byref拷貝函數(shù))首地址, 執(zhí)行 _Block_byref_copy函數(shù), 把棧Block_byref 拷貝到 堆Block_byref
繼續(xù)上一步的位置 內(nèi)存偏移 8字節(jié),得到堆上開辟的 object內(nèi)存空間首地址, 這里當(dāng)然就存放 object對(duì)象了
-
需要注意的一個(gè)細(xì)節(jié)棧Block_byref 拷貝到 堆Block_byref之后,由于堆上是新的內(nèi)存空間,那么棧與堆不就兩個(gè)空間了嗎,如何保障訪問的是同一塊內(nèi)存?就覺辦法就是,在拷貝之后, 把 棧Block_byref 和 堆Block_byref 里的forwarding 都指向了 堆Block_byref, 也就是 堆 forwarding再指向一遍自己
__block修飾變量之后,不管是在block塊內(nèi)訪問變量,還是在block外訪問變量,都是通過 forwarding訪問到堆空間,然后再訪問目標(biāo)空間內(nèi)的 變量, 這樣就保障了 訪問的變量是同一塊內(nèi)存空間
image.png
image.png

-
Block_byref 持有的變量生命周期結(jié)束,執(zhí)行 _Block_object_dispose
- 執(zhí)行_Block_byref_release函數(shù),根據(jù)Block_byref 首地址偏移 找到 Block_byref_2首地址,繼續(xù)偏移8字節(jié) 得到 byref_destroy 執(zhí)行析構(gòu) 回收堆內(nèi)存空間
-
Block_layout 作用域結(jié)束 或者 生命周期結(jié)束, 執(zhí)行 _Block_release
- 根據(jù) Block_layout 首地址偏移 找到 Block_descriptor_2 首地址,繼續(xù)偏移8字節(jié),的到 dispose 執(zhí)行析構(gòu) 回收 堆上開辟的 Block_layout 堆內(nèi)存空間
讀取寄存器查看

符號(hào)斷點(diǎn) _Block_copy

_Block_copy 執(zhí)行之前,寄存器rax接收參數(shù) (arm64 讀取寄存器x1)

執(zhí)行完之后,ret返回,rax寄存器存儲(chǔ)返回值

- 變量a 改為 __block修飾

因?yàn)開_block修飾,Block_layout 中就出現(xiàn)了 copy 函數(shù)地址,通過copy,就執(zhí)行 _Block_copy
而沒有__block修飾,沒有 copy dispose 函數(shù),默認(rèn)執(zhí)行 _Block_copy
造成這個(gè)差別就在于 構(gòu)造傳參時(shí)候 flag的區(qū)別,__block修飾之前 是0,__block修飾之后, 1 << 25




