寫在前面
相信大家對block
都有一定的了解,日常開發中也經常能看到它的身影.本文會從block
概念、blcok
循環引用、block
底層三方面進行講解
一、Block初探
① block定義
帶有自動變量(局部變量)的匿名函數叫做block
,又叫做匿名函數
、代碼塊
② block分類
block
主要有三種類型
-
__NSGlobalBlock__
:全局block
,存儲在全局區
此時的block
無參也無返回值,屬于全局block
-
__NSMallocBlock__
:堆區block
,因為block既是函數,也是對象
此時的block
會訪問外界變量,即底層拷貝a
,所以是堆區block
-
__NSStackBlock__
:棧區block
其中局部變量a
在沒有處理之前(即沒有拷貝之前)是 棧區block
, 處理后(即拷貝之后)是堆區block
,目前的棧區block
越來越少了
這個情況下,可以通過__weak
不進行強持有,block
就還是棧區block
總結:
-
block
直接存儲在全局區 - 如果
block
訪問外界變量,并進行block
相應拷貝,即copy
操作- 如果此時的
block是強引用
,則block
存儲在堆區
,即堆區block
- 如果此時的
block
通過__weak
變成了弱引用,則block
存儲在棧區
,即棧區block
- 如果此時的
二、Block循環引用
① 循環引用的分析
-
正常釋放
:是指A持有B
的引用,當A
調用dealloc
方法,給B
發送release
信號,B
收到release
信號,如果此時B
的retainCount
(即引用計數)為0
時,則調用B
的dealloc
方法
-
循環引用
:A、B
相互持有,所以導致A
無法調用dealloc
方法給B
發送release
信號,而B
也無法接收到release
信號.所以A、B
此時都無法釋放
② 解決循環引用
請問下面兩段代碼有循環引用嗎?
- 代碼一種發
生了循環引用
,因為在block
內部使用了外部變量name
,導致block持有了self
,而self原本是持有block
的,所以導致了self和block的相互持有
. - 代碼二中
無循環引用
,雖然也使用了外部變量,但是self
并沒有持有animation
的block
,僅僅只有animation
持有self
,不構成相互持有.
解決循環引用常見的方式有以下幾種:
- 方式①:
weak-strong-dance
-- 強弱共舞 - 方式②:
__block
修飾對象(需要注意的是在block
內部需要置空
對象,而且block
必須調用) - 方式③: 傳遞對象
self
作為block
的參數,提供給block
內部使用 - 方式④: 使用
NSProxy
②.1 方式①: weak-strong-dance
- 如果
block
內部并未嵌套block
,直接使用__weak
修飾self
即可
此時的weakSelf
和self
指向同一片內存空間
,且使用__weak
不會導致self
的引用計數發生變化,可以通過打印weakSelf
和self
的指針地址,以及self
的引用計數來驗證,如下所示
- 如果
block
內部嵌套block
,需要同時使用__weak
和__strong
其中strongSelf
是一個臨時變量,在block
的作用域內,即內部block
執行完就釋放strongSelf
這種方式屬于打破self對block的強引用
,依賴于中介者模式
,屬于自動置為nil
,即自動釋放
②.2 方式②: __block修飾變量
這種方式同樣依賴于中介者模式
,屬于手動釋放
,是通過__block
修飾對象,主要是因為__block
修飾的對象是可以改變的
需要注意的是這里的block
必須調用,如果不調用block
,vc
就不會置空,那么依舊是循環引用,self
和block
都不會被釋放.
②.3 方式③: 對象self作為參數
主要是將對象self
作為參數,提供給block
內部使用,不會有引用計數問題
②.4 方式④: NSProxy 虛擬類
OC
是只能單繼承
的語言,但是它是基于運行時的機制
,所以可以通過NSProxy
來實現偽多繼承
,填補了多繼承的空白NSProxy
和NSObject
是同級的一個類,也可以說是一個虛擬類
,只是實現了NSObject
的協議-
NSProxy
其實是一個消息重定向封裝的一個抽象類
,類似一個代理人,中間件,可以通過繼承它,并重寫下面兩個方法來實現消息轉發到另一個實例- (void)forwardInvocation:(NSInvocation *)invocation; - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
使用場景
NSProxy
的使用場景主要有兩種
- 實現
多繼承
功能 - 解決了
NSTimer&CADisplayLink
創建時對self
強引用問題,參考YYKit
的YYWeakProxy
循環引用解決原理
主要是通過自定義的NSProxy
類的對象來代替self
,并使用方法實現消息轉發
下面是NSProxy
子類的實現以及使用的場景
- 自定義一個
NSProxy
的子類CJLProxy
- 自定義
TCJCat
類和TCJDog
類
- 通過
TCJProxy
實現多繼承功能
- 通過
TCJProxy
解決定時器中self
的強引用問題
②.5 總結
循環應用的解決方式從根本上來說就兩種,以self -> block -> self
為例
- 打破
self
對block
的強引用,可以block
屬性修飾符使用weak
,但是這樣會導致block
還每創建完就釋放了,所以從這里打破強引用行不通 - 打破
block
對self
的強引用,主要就是self
的作用域和block
作用域的通訊,通訊有代理、傳值、通知、傳參
等幾種方式,用于解決循環,常見的解決方式如下:weak-strong-dance
-
__block
(block
內對象置空,且調用block
) - 將對象
self
作為block
的參數 - 通過
NSProxy
的子類代替self
三、Block底層原理
主要是通過clang
、斷點調試
等方式分析Block
底層
③.1 block本質
- 定義
block.c
文件
- 通過
xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c
,將block.c
編譯成block.cpp
,其中block
在底層被編譯成了以下的形式
通過簡化我們知道:相當于block
等于__main_block_impl_0
,是一個函數
- 查看
__main_block_impl_0
,是一個結構體
,同時可以說明block
是一個__main_block_impl_0
類型的對象,這也是為什么block
能夠%@
打印的原因.
總結:block
的本質
是對象、函數、結構體
,由于block
函數沒有名稱,也被稱為匿名函數
或代碼塊
block
通過clang
編譯后的源碼間的關系如下所示,以__block
修飾的變量為例
③.1.1 block為什么需要調用
在底層block
的類型為__main_block_impl_0
結構體,通過其同名構造函數創建,第一個傳入的block
的內部實現代碼塊,即__main_block_func_0
,用fp
表示,然后賦值給impl
的FuncPtr
屬性,然后在main
中進行了調用,這也是block
為什么需要調用的原因.如果不調用,block
內部實現的代碼塊將無法執行,可以總結為以下兩點
-
函數聲明
:即block
內部實現聲明成了一個函數__main_block_func_0
-
執行具體的函數實現
:通過調用block
的FuncPtr
指針,調用block
執行
③.1.2 block是如何獲取外界變量的
-
定義一個變量,并在
block
中調用 -
底層編譯成下面這樣
__main_block_func_0
中的a
是值拷貝
,如果此時在block
內部實現中作 a++
操作,是有問題的,會造成編譯器的代碼歧義,即此時的a是只讀的
.
總結:block捕獲外界變量時,在內部會自動生成同一個屬性來保存
③.1.3 __block的原理
- 對
a
加一個__block
,然后在block
中對a
進行++
操作
-
main
中的a
是通過外界變量封裝的對象
-
__main_block_impl_0
中,將對象a
的地址&a
給構造函數 - 在
__main_block_func_0
內部對a
的處理是指針拷貝
,此時創建的對象a
與傳入對象的a
指向同一片內存空間
總結:
-
外界變量
通過__block
生成__Block_byref_a_0
結構體 -
結構體
用來保存原始變量的指針和值
- 將變量生成的
結構體對象的指針地址
傳遞給block
,然后在block
內部就可以對外界變量進行操作了
上面__block
和非__block
修飾局部變量產生兩種不同的拷貝
-
非__block修飾
:值拷貝
-深拷貝
,只是拷貝數值,且拷貝的值不可更改,指向不同的內存空間,案例中普通變量a
就是值拷貝
-
__block修飾
:指針拷貝
-淺拷貝
,生成的對象
指向同一片內存空間
,案例中經過__block
修飾的變量a
就是指針拷貝
③.2 分析block源碼所在位置
- 通過在
block
處打斷點,分析運行時的block
,打開匯編
- 加
objc_retainBlock
符號斷點,發現會走到_Block_copy
- 加
_Block_copy
符號斷點,運行斷住,在libsystem_blocks.dylib
源碼中
可以到蘋果開源網站下載最新的libclosure-78源碼,通過查看_Block_copy
的源碼實現,發現block
在底層的真正類型是Block_layout
③.3 Block真正類型 -- Block_layout
查看Block_layout
類型的定義,是一個結構體
-
isa
:指向表明block
類型的類 -
flags
:標識符,按bit
位表示一些block
的附加信息,類似于isa
中的位域,其中flags
的種類有以下幾種,主要重點關注BLOCK_HAS_COPY_DISPOSE
和BLOCK_HAS_SIGNATURE
.BLOCK_HAS_COPY_DISPOSE
決定是否有Block_descriptor_2
.BLOCK_HAS_SIGNATURE
決定是否有Block_descriptor_3
- 第1位:
BLOCK_DEALLOCATING
,釋放標記,-般常用BLOCK_NEEDS_FREE
做位與
操作,一同傳入Flags
, 告知該block
可釋放 - 第16位:
BLOCK_REFCOUNT_MASK
,存儲引用計數的值
;是一個可選用參數 - 第24位:
BLOCK_NEEDS_FREE
,第16是否有效的標志
,程序根據它來決定是否增加或是減少引用計數位的
值; - 第25位:
BLOCK_HAS_COPY_DISPOSE
,是否擁有拷貝輔助函數
(a copy helper function); - 第26位:
BLOCK_HAS_CTOR
,是否擁有block
析構函數; - 第27位:
BLOCK_IS_GC
,標志是否有垃圾回收;//OS X - 第28位:
BLOCK_IS_GLOBAL
,標志是否是全局block
; - 第30位:
BLOCK_HAS_SIGNATURE
,與BLOCK_USE_STRET
相對,判斷當前block
是否擁有一個簽名.用于runtime
時動態調用
- 第1位:
-
reserved
:保留信息,可以理解預留位置,猜測是用于存儲block
內部變量信息 -
invoke
:是一個函數指針
,指向block
的執行代碼 -
descriptor
:block
的附加信息,比如保留變量數
、block的大小
、進行copy
或dispose的輔助函數指針
.有三類-
Block_descriptor_1
是必選的 -
Block_descriptor_2
和Block_descriptor_3
都是可選的
-
以上關于descriptor
的分類可以從其構造函數中體現,其中Block_descriptor_2
和Block_descriptor_3
都是通過Block_descriptor_1
的地址,經過內存平移
得到的
③.4 Block內存變化
- 打斷點運行,打開匯編調式,走到
objc_retainBlock
,block
斷點處讀取寄存器x0
,此時的block
是全局block
,即__NSGlobalBlock__
類型
- 增加外部
變量a
,并在block
內打印
此時讀取block
斷點處讀取寄存器x0
,此時的block
是 棧block
-- __NSStackBlock__
- 執行到符號斷點
objc_retainBlock
時,還是棧區block
- 增加
_Block_copy
符號斷點并斷住,直接在最后的ret
加斷點,讀取x0
,發現經過_Block_copy
之后,變成了堆block
,即__NSMallocBlock__
,主要是因為block
地址發生了改變,為堆block
調用情況
- 同樣也可以通過斷點來驗證
-
register read x0
讀取x0
,為堆block
- 繼續往下走,
register read x9
讀取x9
,還是堆block
- 繼續往下
register read x11
,此時是指向一片內存空間,用于存儲_block_invoke
- 按住
control + step into
,進入_block_invoke
,可以得出是通過內存平移
得到的block
內部實現
前面提到的Block_layout
的結構體源碼,從源碼中可以看出,有個屬性invoke
,即block
的執行者,是從isa
的首地址平移 16字節
取到invoke
,然后進行調用執行的.
③.5 簽名
繼續操作,讀取x0
寄存器,看內存布局,通過 內存平移
3*8
就可獲得Block_layout
的屬性descriptor
,主要是為了查看是否有Block_descriptor_2
和Block_descriptor_3
,其中3
中有block
的簽名
-
register read x0
,讀取寄存器x0
-
po 0x0000000280ad9fb0
,打印block
-
x/8gx 0x0000000280ad9fb0
,即打印block
內存情況
-
x/8gx 0x0000000100024010
, 查看descriptor
的內存情況,其中第三個0x0000000100023396
表示簽名
- 判斷是否有
Block_descriptor_2
,即flags
的BLOCK_HAS_COPY_DISPOSE
(拷貝輔助函數)是否有值-
p/x 1<<25
,即1左移25位
,其十六進制為0x2000000
-
p 0x02000000 & 0x00000000c1000002
,即BLOCK_HAS_COPY_DISPOSE & flags
,等于0
,表示沒有Block_descriptor_2
-
- 判斷是否有
Block_descriptor_3
-
p/x 1<<30
,即1左移30位
-
p 0x40000000 & 0x00000000c1000002
,即BLOCK_HAS_SIGNATURE & flags
,有值,說明有Block_descriptor_3
-
p (char *)0x000000010089f395
-- 獲取Block_descriptor_3
中的屬性signature
簽名
-
po [NSMethodSignature signatureWithObjCTypes:"v8@?0"]
,即打印簽名
其中簽名的部分說明如下
//無返回值
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
//encoding = (@),類型是 @?
type encoding (@) '@?'
//@是isObject ,?是isBlock,代表 isBlockObject
flags {isObject, isBlock}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
//所在偏移位置是8字節
memory {offset = 0, size = 8}
block
的簽名信息類似于方法的簽名信息,主要是體現block
的返回值,參數以及類型等信息.
③.6 block三次copy分析
③.6.1 _Block_copy源碼分析
進入_Block_copy
源碼,將block
從棧區
拷貝至堆區
- 如果需要釋放,則直接釋放
-
block
的引用計數不受runtime
處理的,是由自己管理的 這里可能有個疑問 —— 為什么引用計數是 +2 而不是 +1 ?
因為
flags
的第一號位置已經存儲著釋放標記
-
- 如果是
globalBlock
,則不需要copy
,直接返回 - 反之,只有兩種情況:
棧區block
or堆區block
,由于堆區block
需要申請空間,前面并沒有申請空間的相關代碼,所以只能是棧區block
- 通過
malloc
申請內存空間用于接收block
- 通過
memmove
將block
拷貝至新申請的內存中 - 設置
block
對象的類型為堆區block
,即result->isa = _NSConcreteMallocBlock
- 通過
③.6.2 _Block_object_assign 分析
想要分析block
的三層copy
,首先需要知道外部變量的種類有哪些,在__block的cpp文件中
,在函數聲明時會傳__main_block_desc_0_DATA
結構體,在里面又會去調用__main_block_copy_0
函數,__main_block_copy_0
里面會調用_Block_object_assign
.
對block修飾
其中用的最多的是BLOCK_FIELD_IS_OBJECT
和BLOCK_FIELD_IS_BYREF
而_Block_object_assign
是在底層編譯代碼中,外部變量拷貝時調用的方法就是它
進入_Block_object_assign
源碼
- 1.如果是普通對象,則交給
ARC
處理,并拷貝對象指針
,即引用計數+1
,所以外界變量不能釋放
- 2.如果是
block
類型的,則通過_Block_copy
操作,將block
從棧區拷貝到堆區
- 3.如果是
__block
修飾的變量,調用_Block_byref_copy
函數,進行內存拷貝
以及常規處理
此時捕獲到的變量是被__block
修飾的BLOCK_FIELD_IS_BYREF
類型,就會調用*dest = _Block_byref_copy(object);
- 1.將傳入的對象,強轉為
Block_byref
結構體類型對象,保存一份 - 2.沒有將外界變量
拷貝到堆
,需要申請內存
,其進行拷貝
- 如果已經拷貝過了,則進行處理并返回
- 其中
copy
和src
的forwarding
指針都指向同一片內存
,這也是為什么__block
修飾的對象具有修改能力
的原因 -
(*src2->byref_keep)(copy, src)
-
(*src2->byref_keep)(copy, src)
跟進去會來到Block_byref
結構來,而byref_keep
是Block_byref
的第5個
屬性
-
代碼調試
- 定義一個
__block
修飾的NSString
對象
- 進行
clang
編譯結果如下 - 編譯后的
cj_name
比普通變量多了__Block_byref_id_object_copy_131
和__Block_byref_id_object_dispose_131
-
__Block_byref_cj_name_0
結構體中多了__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
通過上面的分析,我們可以知道這些方法的執行順序_Block_copy
->_Block_byref_copy
->_Block_object_assign
,正好對應上述的三層copy
綜上所述,那么block
是如何拿到cj_name
的呢?
- 1、通過
_Block_copy
方法,將block
拷貝一份至堆區 - 2、通過
_Block_object_assign
方法正常拷貝,因為__block
修飾的外界變量在底層是Block_byref
結構體 - 3、發現外部變量還存有一個對象,從
bref
中取出相應對象cj_name
,拷貝至block
空間,才能使用(相同空間才能使用,不同則不能使用).最后通過內存平移就得到了cj_name
,此時的cj_name
和 外界的cj_name
是同一片內存空間(從_Block_object_assign
方法中的*dest = object;
看出)
三層copy總結
綜上所述,block
的三層拷貝是指以下三層:
- 【第一層】通過
_Block_copy
實現對象的自身拷貝,從棧區拷貝至堆區 - 【第二層】通過
_Block_byref_copy
方法,將對象拷貝為Block_byref
結構體類型 - 【第三層】調用
_Block_object_assign
方法,對__block
修飾的當前變量的拷貝
注意:只有__block
修飾的對象,block
的copy
才有三層
③.6.3 _Block_object_dispose 分析
在__Block_byref_id_object_dispose_131
實現中調用的就是_Block_object_dispose
,下面我們看下_Block_object_dispose
的底層實現:
通過源碼我們可以知道_Block_object_dispose
是進行release
操作,通過不同分區的block
,進行不同的釋放操作
.而_Block_object_assign
是進行retain
操作的.
- 進入
_Block_byref_release
源碼,主要就是對象、變量的釋放銷毀
- 如果是釋放對象就什么也不做(自動釋放)
- 如果是
__block
修飾,就將指向指回原來的區域并使用free釋放
③.6.4 總結
-
block
的本質是個__main_block_impl_0
的結構體對象,所以能用%@
打印 -
block
聲明只是將block
實現保存起來,具體的函數實現需要自行調用
-
block
捕獲外界變量時block
結構體會自動生成一個屬性
來保存變量 -
__block
修飾的屬性在底層會生成響應的結構體,保存原始變量的指針,并傳遞一個指針地址給block
-
block
中有三層拷貝:拷貝block
、拷貝捕獲變量的內存地址
、拷貝對象
Block
的三層copy
的流程如下圖所示
寫在后面
和諧學習,不急不躁.我還是我,顏色不一樣的煙火.