本文為L_Ares個人寫作茬故,以任何形式轉(zhuǎn)載請表明原文出處。
上一節(jié)重新的鞏固了一下Block
的基礎(chǔ)知識和簡單的使用方式晦毙,以及解決循環(huán)引用的方法,本節(jié)則將通過clang
編譯的文件和libclosure
源碼去探索Block
的一些更本質(zhì)的內(nèi)容立润。
準備
1. libclosure源碼
2. block的.cpp
文件
1. 創(chuàng)建一個C語言
的Commond Line Tool
項目霹琼。
2. 語言選擇C
。
3. 創(chuàng)建結(jié)果。
4. 在main.c
中創(chuàng)建一個最簡單的Block
對象首启,并且執(zhí)行Block
。
5. commond + B
進行編譯撤摸,然后打開terminal終端
毅桃,進行clang
編譯成.cpp
文件褒纲。
- 先要進入到
main.c
所在的文件夾下
- 然后打開
terminal終端
,輸入以下clang
指令钥飞。
clang -rewrite-objc main.c -o main.cpp
6. clang結(jié)果
一莺掠、Block的clang探索
通過clang
出來的.cpp
,主要探索4點代承。
- Block的本質(zhì)是什么汁蝶。
- Block()的意義渐扮。
- Block捕獲外部變量的原理论悴。
__block
的原理。
1. Block的本質(zhì)
操作1 :
打開上面
clang
得到的main.cpp
文件墓律“蚬溃滑至文件最后。
結(jié)果1 :
操作2 :
去掉
(void(*)())
和(void *)
的強制類型轉(zhuǎn)換耻讽,Block
的結(jié)構(gòu)就變成了
結(jié)果2 :
操作3 :
commond + F
搜索__main_block_impl_0
察纯。
結(jié)果3 :
至此,可以看到Block塊的一個構(gòu)造方式针肥,其中的__block_impl
結(jié)構(gòu)體存儲了Block塊的所有信息饼记。
操作4 :
commond + F
搜索__block_impl
。
結(jié)果4 :
操作5 :
根據(jù)
圖1.1.2
和圖1.1.3
中慰枕,__block_impl
結(jié)構(gòu)體的存儲屬性具则,以及官方給的注釋,進入libclosure
的Block_private.h
具帮。查找擁有這樣結(jié)構(gòu)的結(jié)構(gòu)體博肋。
結(jié)果5 :
結(jié)論 :
block
的本質(zhì)是Block_layout
結(jié)構(gòu)體。
2. Block()的意義
操作1 :
打開剛才的
.cpp
文件蜂厅,找到block
的構(gòu)造函數(shù)那一行匪凡。
結(jié)果1 :
操作2 :
去掉
block()
經(jīng)過clang
后,得到的那行代碼的所有強轉(zhuǎn)符號掘猿。
結(jié)果2 :
結(jié)論 :
1. 寫在
block
內(nèi)部的函數(shù)被保存到block
的FuncPtr
中病游,這僅僅只是函數(shù)的實現(xiàn)被保存進入block
。
2.block()
才是真正的對函數(shù)進行調(diào)用稠通。
3. Block捕獲外部變量的原理
在main.c
中添加外部變量衬衬,更改main.c
的代碼為 :
#include <stdio.h>
int main(int argc, const char * argv[]) {
int a = 10;
void(^block)(void) = ^{
printf("a = %d",a);
};
block();
return 0;
}
操作1 :
- 重新
commond + B
編譯main.c
文件,然后進行clang
轉(zhuǎn)換成.cpp
文件采记。- 打開新的
.cpp
文件佣耐,滑至文件最后。- 去掉強轉(zhuǎn)唧龄。
結(jié)果1 :
操作2 :
再次查看
block
的構(gòu)造函數(shù)__main_block_impl_0
結(jié)果2 :
操作3 :
查看對外部變量
a
的調(diào)用兼砖。
結(jié)果3 :
結(jié)論 :
1. 當
block
捕獲外部變量的時候奸远,block
結(jié)構(gòu)體內(nèi)部會生成一個相同類型、相同名稱的屬性讽挟。2. 利用
block
自身的構(gòu)造函數(shù)懒叛,將外部變量傳入block
結(jié)構(gòu)體內(nèi)部,用外部變量的值賦值給block
內(nèi)部自動生成的同類型元素耽梅。3.
block
內(nèi)部調(diào)用外部變量時薛窥,在fp
函數(shù)體的內(nèi)部也會生成又一個對象,對外部變量進行值拷貝
眼姐,這個對象和外部變量不是同一個對象诅迷。4. 所以,當我們對
block
內(nèi)部捕獲的外部變量進行操作的時候众旗,是對另外一個對象進行操作罢杉,而不是對外部變量本身進行操作。
4. __block的原理
在main.c
中繼續(xù)修改代碼如下 :
#include <stdio.h>
int main(int argc, const char * argv[]) {
__block int a = 10;
void(^block)(void) = ^{
printf("a = %d",a);
};
block();
return 0;
}
操作1 :
- 重新
commond + B
編譯main.c
文件贡歧,然后進行clang
轉(zhuǎn)換成.cpp
文件滩租。- 打開新的
.cpp
文件,滑至文件最后利朵。- 去掉強轉(zhuǎn)律想。
結(jié)果1 :
操作2 :
搜索
__Block_byref_a_0
結(jié)構(gòu)體。
結(jié)果2 :
操作3 :
查看圖1.4.0中的聲明
block
和block
的構(gòu)造函數(shù)绍弟,以及block()
的實現(xiàn)
結(jié)果3 :
結(jié)論 :
1.
__block
會將外部變量變成一個結(jié)構(gòu)體對象A
技即。2.
結(jié)構(gòu)體對象A
內(nèi)部的__forwarding
存儲著指向外部變量的地址的指針。3.
block
結(jié)構(gòu)體則在內(nèi)部生成新的晌柬、該類型的結(jié)構(gòu)體指針對象B
姥份。4. 通過構(gòu)造函數(shù),利用
結(jié)構(gòu)體對象A
的指針年碘,將結(jié)構(gòu)體對象A
內(nèi)部的__forwarding
賦值給結(jié)構(gòu)體指針對象B
澈歉,也就是把指向外部變量的地址的指針賦值給結(jié)構(gòu)體指針對象B
。5. 當調(diào)用外部變量的時候屿衅,
block
的函數(shù)內(nèi)部會生成一個該類型的結(jié)構(gòu)體指針對象C
埃难,結(jié)構(gòu)體指針對象C
通過結(jié)構(gòu)體指針對象B
的賦值,擁有了指向外部變量的地址的指針涤久。6. 所以在
block
內(nèi)部去改變被__block
修飾的外部變量涡尘,實際上操作的是外部變量的地址上內(nèi)容。7.
__block
是指針拷貝的實現(xiàn)响迂。
二考抄、Block的內(nèi)存變化
在上一節(jié)Block(一)中,已經(jīng)介紹了Block一共有6種類別蔗彤,而開發(fā)者常用的只有其中的3種川梅,分別是 :
1.
NSGlobalBlock
: 全局block
2.NSStackBlock
: 棧block
3.NSMallocBlock
: 堆block
1. NSGlobalBlock
先利用查看匯編疯兼,來查看NSGlobalBlock
的聲明和創(chuàng)建流程。
步驟 :
1. 隨意創(chuàng)建一個項目贫途,在ViewController
的viewDidLoad
中吧彪,簡單的創(chuàng)建一個最基本的block
,在聲明block
的地方掛上斷點丢早。
- (void)viewDidLoad {
[super viewDidLoad];
void(^block)(void) = ^{ //掛上斷點
NSLog(@"this is a block");
};
block();
}
2. 打開xcode
--> Debug
--> Debug Workflow
--> Always Show Disassembly
姨裸,來看匯編。
3. 使用真機
進行調(diào)試怨酝。運行項目傀缩。
4. 對objc_retainBlock
加符號斷點,執(zhí)行到該斷點位置凫碌,查看objc_retainBlock
有怎樣的調(diào)用扑毡。
走到這里就不用再往后走了,看匯編的最后一句 :
5. 此時讀取x0
寄存器(這也是為什么要用真機的原因盛险,模擬器讀不到x0
寄存器)。
結(jié)論 :
1.
NSGlobalBlock
類型的block對象勋又,經(jīng)過objc_retainBlock
苦掘,調(diào)用了_Block_copy
方法,最后將block
對象完成創(chuàng)建并返回楔壤。2. 當
block
沒有獲取外部變量的時候鹤啡,block
是一個NSGlobalBlock
類型的block對象。當block
被在聲明全局變量的地方進行調(diào)用的時候蹲嚣,block
也是NSGlobalBlock
類型递瑰。3.
NSGlobalBlock
存儲位置在內(nèi)存中的靜態(tài)區(qū)(.data區(qū))
2. NSStackBlock和NSMallocBlock
因為編譯器長期處在ARC
環(huán)境下,所以這兩個一起說隙畜,因為不好捕捉NSStackBlock
的瞬間抖部,ARC
會自動的將NSStackBlock
復制到堆中,變成NSMallocBlock
议惰。
步驟 :
1. 對上面NSGlobalBlock
的測試代碼做調(diào)整慎颗,增加一個外部變量,并在block內(nèi)部捕獲外部變量言询。
- (void)viewDidLoad {
[super viewDidLoad];
int a = 10;
void(^block)(void) = ^{ //掛上斷點
NSLog(@"%d",a);
};
block();
}
2. 刪除之前的所有符號斷點俯萎,在上述代碼注釋處掛上斷點,執(zhí)行項目
3. 在objc_retainBlock
內(nèi)部查看此時的寄存器x0
位置运杭,傳入的是什么類型的block
夫啊。
4. 做一個objc_retainBlock
符號斷點,進入objc_retainBlock
辆憔。一直step into
到__Block_copy
撇眯,并在__Block_copy
的最后一句匯編谆趾,也就是那句返回,掛上斷點叛本。然后再讀寄存器x0
結(jié)論 :
1. 當
block
捕獲了外部變量之后沪蓬,block
的類型就從NSGlobalBlock
變成了NSStackBlock
。2.
NSStackBlock
存在于__Block_copy
完成之前来候。3.
__Block_copy
內(nèi)部會將NSStackBlock
變?yōu)?code>NSMallocBlock再返回跷叉。4.
NSStackBlock
和NSMallocBlock
的地址是不一樣的,發(fā)生了一步copy的操作营搅。也就是說云挟,NSStackBlock
通過copy可以得到NSMallocBlock
。
3. 為什么要對NSStackBlock進行copy
直接說結(jié)論
為了延長
block
的生命周期转质。1. 如果
block
的作用域結(jié)束园欣,那么該block
就會被廢棄,其內(nèi)部被__block
修飾的外部變量也會隨之被廢棄休蟹。2. 將
block
從棧區(qū)復制到堆區(qū)沸枯,即使存放在棧區(qū)的block
已經(jīng)被廢棄,堆區(qū)的block
依然可以使用赂弓,被__block
修飾的外部變量也不會被廢棄绑榴。
三、Block的源碼探索
1. Block結(jié)構(gòu)體的解析
在上面我們已經(jīng)知道了Block
的本質(zhì)是Block_layout
結(jié)構(gòu)體盈魁,這里將對這個結(jié)構(gòu)體的屬性做一個介紹翔怎。
看圖3.1.0,block的結(jié)構(gòu)體指針中杨耙,可見5個結(jié)構(gòu)體的屬性赤套。
1.1 isa
這個isa
,在.cpp
的文件中珊膜,賦值的是block
的類型容握,前文說過,block
一共有6種辅搬,分別是 :
1.
_NSConcreteStackBlock
2._NSConcreteMallocBlock
3._NSConcreteAutoBlock
4._NSConcreteFinalizingBlock
5._NSConcreteGlobalBlock
6._NSConcreteWeakBlockVariable
而我們常用的block
類型只屬于其中的3種 :
1.
_NSConcreteGlobalBlock
: 全局Block唯沮,對應NSGlobalBlock
。
2._NSConcreteStackBlock
: 棧Block堪遂,對應NSStackBlock
介蛉。
3._NSConcreteMallocBlock
: 堆Block,對應NSMallocBlock
溶褪。
1.2 flags
這是block的標識位币旧。
- 類型是
int32_t
,表明它有32bit猿妈。- 用
volatile
修飾吹菱,為了保證多線程操作的時候巍虫,flags
永遠都會從內(nèi)存中讀取真正的block
標識數(shù)據(jù),而不是從CPU寄存器中讀取鳍刷,保證數(shù)據(jù)的正確性占遥。
flags
會充分利用內(nèi)存,利用bit位
存儲block
的一些信息输瓜,類似isa
中的ISA_BITFIELD
利用位域存儲isa
信息瓦胎。
flags
的bit位
存儲的內(nèi)容 :
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
第1位 : 存放著釋放標記。一般利用
BLOCK_NEEDS_FREE
(1左移24位
)尤揣,做位與
操作搔啊,再存入該位,記錄block
是否需要釋放北戏。第16位 : 存儲
block
的引用計數(shù)的值负芋。第24位 : 是否需要釋放
block
。它會影響第1位的值嗜愈,也會影響第16位的值旧蛾。第25位 : 是否擁有拷貝輔助函數(shù)。
第26位 : 是否擁有
block
析構(gòu)函數(shù)芝硬。第27位 : 是否有垃圾回收蚜点。
第28位 : 是否是全局變量。
第30位 :
block
是否擁有一個簽名拌阴。如果沒有簽名,則第29位也不會被定義使用奶镶。
1.3 reserved
我也不知道干什么用的迟赃,如果有大佬知道的話,可以賜教厂镇,萬分感謝纤壁。
1.4 invoke
看其類型——BlockInvokeFunction
typedef void(*BlockInvokeFunction)(void *, ...);
重定義類型的函數(shù)指針,也就是存儲在block
塊內(nèi)部的函數(shù)捺信。
1.5 descriptor
block
的描述信息酌媒,是結(jié)構(gòu)體。擁有3種descriptor
迄靠,其中秒咨,Block_descriptor_1
是block
一定擁有的描述信息。
1.5.1 Block_descriptor_1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
存放了一個保留值和一個block
的大小掌挚。
1.5.2 Block_descriptor_2
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
存放了一個copy
方法和一個dispose
方法雨席。
官方注釋 : 當flags
中的BLOCK_HAS_COPY_DISPOSE
,也就是flags
第25位為1的時候吠式,block
就會擁有Block_descriptor_2
描述陡厘。
1.5.3 Block_descriptor_3
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
存放了block
的簽名和layout
抽米,layout
依賴于flags
的第31位是否為1。
官方注釋 : 當falgs
中的BLOCK_HAS_SIGNATURE
糙置,也就是flags
的第30位為1的時候云茸,block
就會擁有Block_descriptor_3
。
2. Block的簽名
OC對象都是有簽名的谤饭,既然說Block
是OC對象标捺,那么Block
也必然有簽名,Block
簽名存儲在Block
結(jié)構(gòu)體的Block_descriptor_3
屬性中网持。
下面來驗證宜岛、并且獲得Block
的簽名。
操作1 :
- 隨便創(chuàng)建一個項目功舀,并在
ViewController
的viewDidLoad
中創(chuàng)建一個block
萍倡。- 在下面代碼注釋處添加斷點。
- 打開匯編辟汰。
xcode
-->Debug
-->Debug Workflow
-->Always Show Disassembly
- 連接真機列敲,執(zhí)行代碼。
lldb
打印x0
寄存器中的內(nèi)容帖汞。
- (void)viewDidLoad {
[super viewDidLoad];
void(^block)(void) = ^{ //掛上斷點
NSLog(@"this is a block");
};
block();
}
結(jié)果1 :
這里已經(jīng)可以獲得Block
的type encoding
了戴而,就是圖3.2.0中的signature : "v8@?0"
。關(guān)于type encoding
可以看我的這片博客關(guān)于Objective-C type encoding翩蘸。
其中v8
表示void
總共占用8位所意,@?
表示一個不明類型的對象,0
表示@?
是在這個函數(shù)的第0位地址上催首。
那么扶踊,利用 :
[NSMethodSignature signatureWithObjCTypes:"@?"];
就可以獲得Block
的簽名信息。
操作2 :
在
lldb
上po [NSMethodSignature signatureWithObjCTypes:"@?"]
結(jié)果2 :
結(jié)論 :
1. Block擁有簽名郎任,并且存儲在
Block_descriptor_3
中秧耗。
2. Block的type encoding
是@?
。
3. Block的簽名是isObject
,isBlock
舶治。