iOS之武功秘籍?: Block的原理

iOS之武功秘籍 文章匯總

寫在前面

相信大家對block都有一定的了解,日常開發中也經常能看到它的身影.本文會從block概念、blcok循環引用、block底層三方面進行講解

本節可能用到的秘籍Demo

一、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信號,如果此時BretainCount(即引用計數)為0時,則調用Bdealloc方法
  • 循環引用A、B相互持有,所以導致A無法調用dealloc方法給B發送release信號,而B也無法接收到release信號.所以A、B此時都無法釋放

② 解決循環引用

請問下面兩段代碼有循環引用嗎?


  • 代碼一種發生了循環引用,因為在block內部使用了外部變量name,導致block持有了self,而self原本是持有block的,所以導致了self和block的相互持有.
  • 代碼二中無循環引用,雖然也使用了外部變量,但是self并沒有持有animationblock,僅僅只有animation持有self,不構成相互持有.

解決循環引用常見的方式有以下幾種:

  • 方式①: weak-strong-dance -- 強弱共舞
  • 方式②: __block修飾對象(需要注意的是在block內部需要置空對象,而且block必須調用)
  • 方式③: 傳遞對象self作為block的參數,提供給block內部使用
  • 方式④: 使用NSProxy

②.1 方式①: weak-strong-dance

  • 如果block內部并未嵌套block,直接使用__weak修飾self即可

此時的weakSelfself指向同一片內存空間,且使用__weak不會導致self的引用計數發生變化,可以通過打印weakSelfself的指針地址,以及self的引用計數來驗證,如下所示

  • 如果block內部嵌套block,需要同時使用__weak__strong

其中strongSelf是一個臨時變量,在block的作用域內,即內部block執行完就釋放strongSelf

這種方式屬于打破self對block的強引用,依賴于中介者模式,屬于自動置為nil,即自動釋放

②.2 方式②: __block修飾變量

這種方式同樣依賴于中介者模式,屬于手動釋放,是通過__block修飾對象,主要是因為__block修飾的對象是可以改變的

需要注意的是這里的block必須調用,如果不調用blockvc就不會置空,那么依舊是循環引用,selfblock都不會被釋放.

②.3 方式③: 對象self作為參數

主要是將對象self作為參數,提供給block內部使用,不會有引用計數問題

②.4 方式④: NSProxy 虛擬類

  • OC是只能單繼承的語言,但是它是基于運行時的機制,所以可以通過NSProxy來實現偽多繼承,填補了多繼承的空白

  • NSProxyNSObject是同級的一個類,也可以說是一個虛擬類,只是實現了NSObject的協議

  • NSProxy其實是一個消息重定向封裝的一個抽象類,類似一個代理人,中間件,可以通過繼承它,并重寫下面兩個方法來實現消息轉發到另一個實例

    - (void)forwardInvocation:(NSInvocation *)invocation;
    - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
    

使用場景

NSProxy的使用場景主要有兩種

  • 實現多繼承功能
  • 解決了NSTimer&CADisplayLink創建時對self強引用問題,參考YYKitYYWeakProxy

循環引用解決原理
主要是通過自定義的NSProxy類的對象來代替self,并使用方法實現消息轉發

下面是NSProxy子類的實現以及使用的場景

  • 自定義一個NSProxy的子類CJLProxy
  • 自定義TCJCat類和TCJDog
  • 通過TCJProxy實現多繼承功能
  • 通過TCJProxy解決定時器中self的強引用問題

②.5 總結

循環應用的解決方式從根本上來說就兩種,以self -> block -> self為例

  • 打破selfblock的強引用,可以block屬性修飾符使用weak,但是這樣會導致block還每創建完就釋放了,所以從這里打破強引用行不通
  • 打破blockself的強引用,主要就是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表示,然后賦值給implFuncPtr屬性,然后在main中進行了調用,這也是block為什么需要調用的原因.如果不調用,block內部實現的代碼塊將無法執行,可以總結為以下兩點

  • 函數聲明:即block內部實現聲明成了一個函數__main_block_func_0
  • 執行具體的函數實現:通過調用blockFuncPtr指針,調用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_DISPOSEBLOCK_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 時動態調用
  • reserved:保留信息,可以理解預留位置,猜測是用于存儲block內部變量信息
  • invoke:是一個函數指針,指向block的執行代碼
  • descriptorblock的附加信息,比如保留變量數block的大小進行copydispose的輔助函數指針.有三類
    • Block_descriptor_1是必選的
    • Block_descriptor_2Block_descriptor_3都是可選的

以上關于descriptor的分類可以從其構造函數中體現,其中Block_descriptor_2Block_descriptor_3都是通過Block_descriptor_1的地址,經過內存平移得到的

③.4 Block內存變化

  • 打斷點運行,打開匯編調式,走到objc_retainBlockblock斷點處讀取寄存器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_2Block_descriptor_3,其中3中有block的簽名

  • register read x0,讀取寄存器x0
  • po 0x0000000280ad9fb0,打印block
  • x/8gx 0x0000000280ad9fb0 ,即打印block內存情況
  • x/8gx 0x0000000100024010 , 查看descriptor的內存情況,其中第三個0x0000000100023396表示簽名
  • 判斷是否有Block_descriptor_2,即flagsBLOCK_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
    • 通過memmoveblock拷貝至新申請的內存中
    • 設置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_OBJECTBLOCK_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.沒有將外界變量拷貝到堆,需要申請內存,其進行拷貝
  • 如果已經拷貝過了,則進行處理并返回
  • 其中copysrcforwarding指針都指向同一片內存,這也是為什么__block修飾的對象具有修改能力的原因
  • (*src2->byref_keep)(copy, src)
    • (*src2->byref_keep)(copy, src)跟進去會來到Block_byref結構來,而byref_keepBlock_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修飾的對象,blockcopy才有三層

③.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的流程如下圖所示

寫在后面

和諧學習,不急不躁.我還是我,顏色不一樣的煙火.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容