本文為L_Ares個人寫作溺健,以任何形式轉(zhuǎn)載請表明原文出處。
準備
1. libclosure源碼
2. block的.cpp
文件
clang
獲取存在block
的.cpp
文件的方法在上一篇文中有專門的教程阎抒。
一、關(guān)于全局Block
前兩節(jié)中,我們已經(jīng)了解了常用的Block
的3種常見的分類返敬,也知道了全局Block
如果捕獲了外界變量的話,就會從全局Block
變成棧Block
寥院。
其實對于Block
的類型判斷是由編譯器進行分辨的劲赠,在編譯期的時候 :
- 如果
Block
并未進行捕獲外界變量的操作,那么Block
就會被認為是NSGlobalBlock
秸谢。- 對于帶有參數(shù)的
Block
凛澎,參數(shù)不屬于外界變量,屬于Block
的內(nèi)部變量估蹄,所以類型也是NSGlobalBlock
塑煎。- 當
Block
對外界變量進行了捕獲之后,編譯器會對Block
的類型判斷變成NSStackBlock
臭蚁。- 對
Block
是否是全局變量的判斷最铁,會存儲在Block
的結(jié)構(gòu)體屬性flags
中讯赏,在其第28位上。- 編譯器不會對
NSGlobalBlock
主動做copy
操作冷尉,即使開發(fā)者手動對NSGlobalBlock
進行copy
方法的調(diào)用待逞,NSGlobalBlock
也不會發(fā)生類型的改變。
二网严、棧Block變成堆Block的源碼解析
在上面识樱,我們已經(jīng)知道了,全局Block也就是NSGlobalBlock
和堆震束、棧的Block
是的區(qū)分條件手段 :
編譯器的在編譯期就通過對
Block
是否捕獲外界變量進行區(qū)分怜庸。
而對于捕獲外界變量的棧和堆Block
,在上一節(jié)的Block內(nèi)存變化中垢村,已經(jīng)通過匯編的分析割疾,知道了一個條件 :
在聲明
Block
的時候,會通過objc_retainBlock
的_Block_copy
將NSStackBlock
變成NSMallocBlock
嘉栓。
那么這里就會通過libclosure
源碼探索一下_Block_copy
的實現(xiàn)思路宏榕。
操作 :
在準備好的
libclosure
源碼中全局搜索_Block_copy
,找到其在runtime.cpp
文件中的函數(shù)實現(xiàn)侵佃。
結(jié)果 :
結(jié)論 :
對于Block的自身從棧區(qū)拷貝到堆區(qū) :
- Block塊的復(fù)制首先要把需要被復(fù)制的Block轉(zhuǎn)成Block的本質(zhì)結(jié)構(gòu)
Block_layout
結(jié)構(gòu)體麻昼。- 然后判斷Block塊的類型屬性
- 如果已經(jīng)是堆Block了,那么只利用自身的引用計數(shù)管理馋辈,改變Block的flags中的第2位
BLOCK_REFCOUNT_MASK
就可以抚芦。- Block的引用計數(shù)管理是自己進行管理,不實用runtime底層的引用計數(shù)方式迈螟。
- 如果是全局Block叉抡,則不發(fā)生任何的拷貝,直接返回原Block答毫。
- 如果是棧Block褥民,則在堆區(qū)申請一塊和原來棧Block內(nèi)存大小一樣的內(nèi)存,利用位拷貝洗搂,將棧Block塊整體的拷貝到堆區(qū)消返,保證堆Block和棧Block的數(shù)據(jù)完全一致。
- 并且重置引用計數(shù)為1蚕脏,為了讓內(nèi)存工具可以看到完整正確的Block信息侦副,最后才將Block的isa指向
NSMallocBlock
類。
三驼鞭、Block捕獲的外界變量的copy
在上面秦驯,我們已經(jīng)知道了Block塊是如何從棧區(qū)拷貝到堆區(qū)的,在上面的圖2.0.1中挣棕,可以找到Block的isa
译隘、flags
亲桥、invoke
都很明顯的進行了從棧區(qū)到堆區(qū)的拷貝,那么除了一個保留值reserved
固耘,還有一個Block的描述并沒有明顯的進行拷貝而是調(diào)用了_Block_call_copy_helper
题篷。
對于被Block捕獲的外界變量也需要隨Block一起拷貝到堆區(qū),才能保證外界變量的生命周期在Block內(nèi)部得以延長厅目,也就是說番枚,_Block_call_copy_helper
的這一步就代表著要對已然存儲在棧Block上的外界變量copy到堆Block上。
問題 :
Block捕獲的外界變量是如何隨著棧區(qū)Block拷貝到堆區(qū)Block上的损敷?
已知條件 :
_Block_call_copy_helper
的調(diào)用會讓Block捕獲的外界變量隨著棧區(qū)Block一起拷貝到堆區(qū)Block上葫笼。
操作 :
逐步進入
_Block_call_copy_helper
的實現(xiàn),找到和copy
相關(guān)的線索拗馒。
結(jié)果 :
由已知條件可知思路 :
- 從Block的構(gòu)造函數(shù)找到
descriptor2
的賦值路星。- 從
descriptor2
找到被Block捕獲的外界變量是如何隨著Block一起拷貝到堆上的。
進行探索 :
操作1 :
- 創(chuàng)建一個
iOS
的Project
诱桂。- 在
main.m
文件中直接寫入以下代碼洋丐。- 通過
clang
將main.m
文件編譯成main.cpp
文件,進度條拉到最后挥等,查看__block
的c++
實現(xiàn)友绝。- 刪除掉
main
函數(shù)中無關(guān)的代碼,刪除強轉(zhuǎn)触菜,只保留Block
相關(guān)的代碼九榔。
-
main.m
代碼 :
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
//這里不要直接用jd_name = @"JD"
//要用[NSString stringWithFormat:@"JD"],否則clang未必能編譯成功
__block NSString *jd_name = [NSString stringWithFormat:@"JD"];
void(^jd_block)(void) = ^{
jd_name = @"eason";
NSLog(@"%@",jd_name);
};
jd_block();
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
-
clang
命令涡相,這里注意,main.m
的代碼中明顯的有<UIKit>
框架的使用剩蟀,直接用以前的clang
命令是無法編譯到<UIKit>
框架的催蝗,所以clang
指令會變成下面的 :
xcrun -sdk iphonesimulator clang -rewrite-objc main.m
結(jié)果1 :
操作2 :
搜索Block構(gòu)造函數(shù)的第二個參數(shù)
__main_block_desc_0_DATA
。
結(jié)果2 :
操作3 :
搜索
__main_block_desc_0_DATA
的屬性值__main_block_copy_0
和__main_block_dispose_0
育特。
結(jié)果3 :
發(fā)現(xiàn)拷貝輔助函數(shù)丙号,也就是
Block_descriptor_2
存儲的是對__block
修飾的外界變量進行assign
和dispose
的函數(shù)——_Block_object_assign()
和_Block_object_dispose()
。
操作4 :
在
libclosure
源碼中搜索_Block_object_assign
缰冤。找到拷貝輔助函數(shù)的實現(xiàn)犬缨。
結(jié)果4 :
操作4.1 :
首先,看一下拷貝輔助函數(shù)的
switch
條件棉浸,也就是BLOCK_ALL_COPY_DISPOSE_FLAGS
是什么怀薛。
結(jié)果4.1 :
enum {
BLOCK_ALL_COPY_DISPOSE_FLAGS =
BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_BYREF |
BLOCK_FIELD_IS_WEAK | BLOCK_BYREF_CALLER
};
enum {
BLOCK_FIELD_IS_OBJECT = 3,
BLOCK_FIELD_IS_BLOCK = 7,
BLOCK_FIELD_IS_BYREF = 8,
BLOCK_FIELD_IS_WEAK = 16,
BLOCK_BYREF_CALLER = 128
}
1.
BLOCK_FIELD_IS_OBJECT = 3
: 對象
2.BLOCK_FIELD_IS_BLOCK = 7
: 普通變量
3.BLOCK_FIELD_IS_BYREF = 8
: __block修飾的結(jié)構(gòu)體
4.BLOCK_FIELD_IS_WEAK = 16
: __weak修飾的變量
5.BLOCK_BYREF_CALLER = 128
: 處理Block_byref結(jié)構(gòu)體內(nèi)部對象的內(nèi)存的時候添加的額外標記,要配合上面的枚舉一起使用到
操作4.2 :
查看Block捕獲對象的時候迷郑,是如何進行拷貝的枝恋。也就是圖3.0.5中
_Block_retain_object
的實現(xiàn)创倔。
結(jié)果4.2 :
操作4.3 :
查看Block捕獲普通變量的時候,是如何進行拷貝的焚碌。也就是圖3.0.5中的
_Block_copy
的實現(xiàn)畦攘。
結(jié)果4.3 :
在上面的二、棧Block變成堆Block的源碼解析 中已經(jīng)介紹過了十电。
操作4.4 :
查看Block捕獲經(jīng)
__block
修飾的變量的時候知押,是如何進行拷貝的。
結(jié)果4.4 :
操作5 :
這里繼續(xù)對
__block
修飾的變量進行拷貝的實現(xiàn)的探索鹃骂。進入_Block_byref_copy
的實現(xiàn)台盯。
結(jié)果5 :
Tips :
還記得上一節(jié)提到的,
__block
修飾的變量偎漫,在Block函數(shù)內(nèi)部可以進行修改的原因是指針的copy爷恳,而指針則必然指向變量的值所在的地址,更改的是這個地址上的值象踊。那么這個地址上的數(shù)據(jù)也要跟隨Block從椢虑祝拷貝到堆才對。
操作6 :
- 在匯編的
.cpp
文件中找到__Block_byref_jd_name_0
結(jié)構(gòu)體杯矩。- 然后在
libclosure
中找到Block_byref
結(jié)構(gòu)體栈虚。
結(jié)果6 :
可以看到,Block捕獲的外界變量在匯編后被轉(zhuǎn)為的結(jié)構(gòu)體的本質(zhì)就是Block_byref
結(jié)構(gòu)體史隆。它的結(jié)構(gòu)設(shè)計和Block塊的結(jié)構(gòu)設(shè)計是及其類似的魂务。
操作7 :
- 根據(jù)圖3.0.7和圖3.0.8,在圖3.0.7中泌射,發(fā)現(xiàn)了這樣一步調(diào)用
(*src2->byref_keep)(copy, src);
粘姜,在圖3.0.8中,可以知道byref_keep
函數(shù)是__Block_byref_id_object_copy
熔酷。- 在
.cpp
中搜索__Block_byref_id_object_copy
函數(shù)的實現(xiàn)孤紧。
結(jié)果7 :
操作8 :
- 可以看到,對
Block_byref
結(jié)構(gòu)體中的NSString *jd_name
的copy
還是利用圖3.0.4中的方法拒秘,并且這次走的是第一個case
号显,也就是BLOCK_FIELD_IS_OBJECT
的copy
。原因很簡單躺酒,看畫紅框的+40押蚤,就是讓Block_byref
結(jié)構(gòu)體的指針偏移40字節(jié)。而Block_byref
結(jié)構(gòu)體指針偏移40字節(jié)就是NSString *jd_name;
的地址羹应。- 所以揽碘,這一步就完成了
__block
捕獲的外界變量的copy操作。
結(jié)果8 :
四、總結(jié)
- 全局Block和堆棧Block的區(qū)分是編譯器在編譯期做出的判斷
- 沒有捕獲外界變量的Block和定義在全局區(qū)的Block都是全局Block钾菊。
- 捕獲了外界變量的Block是棧Block帅矗。
- 為了延長棧Block中的捕獲到的外界變量的生命周期,防止操作系統(tǒng)的自動釋放煞烫,所以將棧Block拷貝到堆Block浑此,并把棧Block的指針指向堆Block,這樣更穩(wěn)定滞详、更安全凛俱。
- 棧Block拷貝到堆Block是調(diào)用了
_Block_copy
函數(shù),將整個Block塊拷貝到堆上料饥。- 堆Block擁有引用計數(shù)蒲犬,并且由Block的
flags
屬性進行記錄管理,不使用runtime
底層的引用計數(shù)管理岸啡。- 對于Block捕獲外界變量原叮,分為兩種情況,一種是沒有
__block
修飾的外界變量巡蘸。一種是有__block
修飾的外界變量奋隶。無論是否有__block
的修飾,它們的共同點都是可能會利用存儲在descriptor2
中的拷貝輔助函數(shù)悦荒,將存儲在棧Block上的外界變量拷貝到堆Block上唯欣。
- 4.1 對于沒有
__block
修飾的外界變量
- 就當作是對象進行拷貝,外界變量的引用計數(shù)依然是
runtime
進行管理搬味。- 調(diào)用的是
_Block_object_assign
函數(shù)境氢。- 4.2 對于擁有
__block
修飾的外界變量
- 需要兩次復(fù)制,先進行外界變量的結(jié)構(gòu)體的copy碰纬,也就是
Block_byref
對象的拷貝萍聊。- 再利用
Block_byref
結(jié)構(gòu)體中的拷貝輔助函數(shù),對外界變量結(jié)構(gòu)體中真正的外界變量進行copy悦析。- 調(diào)用的也是
_Block_object_assign
函數(shù)脐区。