本文翻譯自Matt Galloway的博客洗出,借此機會學習一下Block的內(nèi)部原理。
今天我們從編譯器的視角來研究一下Block的內(nèi)部是怎么工作的菠镇。這里說的Blocks指的是Apple為C語言添加的閉包辟犀,而且現(xiàn)在從clang/LLVM角度來說已經(jīng)成為了語言的一部分堂竟。我一直很好奇Block到底是什么以及怎樣被視為一個Objective-C
對象的(你可以對它們執(zhí)行copy
,retain
税稼,release
操作郎仆。)這篇博客來稍微研究一下Block。
基礎
下面代碼是一個Block:
void(^block)(void) = ^{
NSLog(@"I'm a block!");
};
它創(chuàng)建了一個叫做block
的變量曙旭,而且用一個簡單的代碼塊賦值給它桂躏。這很簡單。這就完成了进倍?不猾昆,我想了解編譯器為這一小段代碼干了什么事垂蜗。
此外烘苹,你也可以給block傳遞一個參數(shù):
void(^block)(int a) = ^{
NSLog(@"I'm a block! a = %i", a);
};
甚至還可以反悔一個值:
int(^block)(void) = ^{
NSLog(@"I'm a block!");
return 1;
};
作為一個閉包镣衡,它們捕獲了它們的上下文:
int a = 1;
void(^block)(void) = ^{
NSLog(@"I'm a block! a = %i", a);
};
那么編譯器是怎樣組織這所有部分的呢?這正是我感興趣的惰说。
深究一個簡單的示例
我的第一個想法是看看編譯器怎樣編譯一個非常簡單的block的吆视,比如下例代碼:
#import <dispatch/dispatch.h>
typedef void(^BlockA)(void);
__attribute__((noinline))
void runBlockA(BlockA block) {
block();
}
void doBlockA() {
BlockA block = ^{
// Empty block
};
runBlockA(block);
}
搞兩個方法是因為我想看看一個block是如何被創(chuàng)建以及如何被調(diào)用的。如果兩者都放在一個方法里面丰滑,編譯優(yōu)化器可能比較聰明,那我們就看不到有趣的現(xiàn)象了。我必須聲明runBlock
為noinline
的绍申,否則優(yōu)化器會把它內(nèi)聯(lián)到doBlock
方法中极阅,這會導致上述同樣的問題仆百。
上述代碼編譯出來的匯編代碼如下(編譯器是armv7,03):
.globl _runBlockA
.align 2
.code 16 @ @runBlockA
.thumb_func _runBlockA
_runBlockA:
@ BB#0:
ldr r1, [r0, #12]
bx r1
這是runBlockA
部分俄周,非常的簡單〔ㄊ疲回顧一下源碼尺铣,這個方法僅僅調(diào)用了一個block。寄存器r0
在ARM EABI中被設置為這個方法的第一個參數(shù)侄非。因此第一條指令意味著r1
是從r0 + 12
的地址處加載的逞怨。可以認為這是一個指針的間接引用除秀,讀入12個字節(jié)進去。然后我們跳轉到哪個地址暂吉。注意使用的是r1
慕的,意味著r0
仍然是參數(shù)block自身风题。所以這看起來就像是正在調(diào)用的方法把這個block作為第一個參數(shù)俯邓。
從這里我可以確定block很可能是一些結構體組成,實際執(zhí)行的方法是存儲在相應結構體里面的12個字節(jié)朦蕴。當傳遞一個block時吩抓,實際上傳遞的是指向某一個結構體的指針。
現(xiàn)在來看看doBlock
方法:
.globl _doBlockA
.align 2
.code 16 @ @doBlockA
.thumb_func _doBlockA
_doBlockA:
movw r0, :lower16:(___block_literal_global-(LPC1_0+4))
movt r0, :upper16:(___block_literal_global-(LPC1_0+4))
LPC1_0:
add r0, pc
b.w _runBlockA
好吧伦连,這也非常簡單雨饺。這是一個程序計數(shù)器相對加載(?)惑淳。你可以認為這就是把變量___block_literal_global
的地址加載到r0
额港。然后調(diào)用了_runBlockA
方法。我們已經(jīng)知道r0
當作block對象被傳遞給_runBlockA
了歧焦,那___block_literal_global
一定就是那個block對象。
現(xiàn)在我們已經(jīng)取得一些進展了绢馍!但是___block_literal_global
是個什么東西向瓷?通過匯編代碼我們發(fā)現(xiàn):
.align 2 @ @__block_literal_global
___block_literal_global:
.long __NSConcreteGlobalBlock
.long 1342177280 @ 0x50000000
.long 0 @ 0x0
.long ___doBlockA_block_invoke_0
.long ___block_descriptor_tmp
啊哈!那看起來簡直太像是一個結構體了舰涌。這個結構體里有5個值猖任,每一個都是4字節(jié)大小。這肯定就是runBlockA
操作的block對象舵稠。再看,結構體的第12個字節(jié)叫做___doBlockA_block_invoke_0
的東西疑似一個函數(shù)指針。如果你還記得哺徊,那就是上述runBlockA
所跳轉的地方室琢。
然而,什么又是__NSConcreteGlobalBlock
落追?這個我們后面再說盈滴。我們更感興趣的是___doBlockA_block_invoke_0
和 ___block_descriptor_tmp
。
.align 2
.code 16 @ @__doBlockA_block_invoke_0
.thumb_func ___doBlockA_block_invoke_0
___doBlockA_block_invoke_0:
bx lr
.section __DATA,__const
.align 2 @ @__block_descriptor_tmp
___block_descriptor_tmp:
.long 0 @ 0x0
.long 20 @ 0x14
.long L_.str
.long L_OBJC_CLASS_NAME_
.section __TEXT,__cstring,cstring_literals
L_.str: @ @.str
.asciz "v4@?0"
.section __TEXT,__objc_classname,cstring_literals
L_OBJC_CLASS_NAME_: @ @"\01L_OBJC_CLASS_NAME_"
.asciz "\001"
___doBlockA_block_invoke_0
疑似block的真正實現(xiàn)部分轿钠,因為我們用的是一個空的block巢钓。這個方法直接返回了,這正是我們期望一個空方法應該被編譯的樣子疗垛。
再看看___block_descriptor_tmp
症汹。這又是一個結構體,有4個值贷腕。第二值是20背镇,正是___block_literal_global
結構體的大小≡笊眩可能那就是一個size的值瞒斩?還有一個C字符串.str
值為v4@?0
,看起來像是一個類型的編碼格式涮总⌒卮眩可能是一個block的編碼(比如返回空,不帶參數(shù)...)瀑梗。其他的值暫時不管烹笔。
源碼就在那里,不是嗎夺克?
是的箕宙,源碼就在那。它是LLVM里compiler-rt
項目的一部分铺纽。梳理代碼后我發(fā)現(xiàn)了Block_private.h
里的如下定義:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
看起來簡直太熟悉了柬帕!Block_layout
結構體就是我們之前說的___block_literal_global
,Block_descriptor
結構體就是___block_descriptor_tmp
狡门。而且陷寝,我猜對了descriptor的第二個值就是size。Block_descriptor
的第三個和第四個值有點奇怪其馏。它們看起來應該是函數(shù)指針凤跑,但是我們編譯階段看到的是兩個字符串。暫時先忽略它們叛复。
Block_layout
的isa
很有趣仔引,它一定就是_NSConcreteGlobalBlock
扔仓,而且一定是block視作一個一個Objective-C
對象的原因。如果_NSConcreteGlobalBlock
是一個類咖耘,那么OC的消息分發(fā)機制一定樂于把block當作一個普通的對象翘簇。這類似于toll-free bridging的工作原理。
總結起來儿倒,編譯器好像用如下的邏輯來處理代碼:
#import <dispatch/dispatch.h>
__attribute__((noinline))
void runBlockA(struct Block_layout *block) {
block->invoke();
}
void block_invoke(struct Block_layout *block) {
// Empty block function
}
void doBlockA() {
struct Block_descriptor descriptor;
descriptor->reserved = 0;
descriptor->size = 20;
descriptor->copy = NULL;
descriptor->dispose = NULL;
struct Block_layout block;
block->isa = _NSConcreteGlobalBlock;
block->flags = 1342177280;
block->reserved = 0;
block->invoke = block_invoke;
block->descriptor = descriptor;
runBlockA(&block);
}
太好了版保,現(xiàn)在我們已經(jīng)更多地了解了block底層是如何工作的。