本文翻譯自Matt Galloway的部落格,藉此機會學習一下Block的內部原理。
今天我們從編譯器的視角來研究一下Block的內部是怎麼工作的。這裡說的Blocks指的是Apple為C語言新增的閉包,而且現在從clang/LLVM角度來說已經成為了語言的一部分。我一直很好奇Block到底是什麼以及怎樣被視為一個Objective-C
物件的(你可以對它們執行copy
,retain
,release
操作。)這篇部落格來稍微研究一下Block。
基礎
下面程式碼是一個Block:
void(^block)(void) = ^{
NSLog(@"I'm a block!");
};複製程式碼
它建立了一個叫做block
的變數,而且用一個簡單的程式碼塊賦值給它。這很簡單。這就完成了?不,我想了解編譯器為這一小段程式碼幹了什麼事。
此外,你也可以給block傳遞一個引數:
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是如何被建立以及如何被呼叫的。如果兩者都放在一個方法裡面,編譯優化器可能比較聰明,那我們就看不到有趣的現象了。我必須宣告runBlock
為noinline
的,否則優化器會把它內聯到doBlock
方法中,這會導致上述同樣的問題。
上述程式碼編譯出來的彙編程式碼如下(編譯器是armv7,03):
.globl _runBlockA
.align 2
.code 16 @ @runBlockA
.thumb_func _runBlockA
_runBlockA:
@ BB#0:
ldr r1, [r0, #12]
bx r1複製程式碼
這是runBlockA
部分,非常的簡單。回顧一下原始碼,這個方法僅僅呼叫了一個block。暫存器r0
在ARM EABI中被設定為這個方法的第一個引數。因此第一條指令意味著r1
是從r0 + 12
的地址處載入的。可以認為這是一個指標的間接引用,讀入12個位元組進去。然後我們跳轉到哪個地址。注意使用的是r1
,意味著r0
仍然是引數block自身。所以這看起來就像是正在呼叫的方法把這個block作為第一個引數。
從這裡我可以確定block很可能是一些結構體組成,實際執行的方法是儲存在相應結構體裡面的12個位元組。當傳遞一個block時,實際上傳遞的是指向某一個結構體的指標。
現在來看看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複製程式碼
好吧,這也非常簡單。這是一個程式計數器相對載入(?)。你可以認為這就是把變數___block_literal_global
的地址載入到r0
。然後呼叫了_runBlockA
方法。我們已經知道r0
當作block物件被傳遞給_runBlockA
了,那___block_literal_global
一定就是那個block物件。
現在我們已經取得一些進展了!但是___block_literal_global
是個什麼東西?通過彙編程式碼我們發現:
.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位元組大小。這肯定就是runBlockA
操作的block物件。再看,結構體的第12個位元組叫做___doBlockA_block_invoke_0
的東西疑似一個函式指標。如果你還記得,那就是上述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的真正實現部分,因為我們用的是一個空的block。這個方法直接返回了,這正是我們期望一個空方法應該被編譯的樣子。
再看看___block_descriptor_tmp
。這又是一個結構體,有4個值。第二值是20,正是___block_literal_global
結構體的大小。可能那就是一個size的值?還有一個C字串.str
值為v4@?0
,看起來像是一個型別的編碼格式。可能是一個block的編碼(比如返回空,不帶引數...)。其他的值暫時不管。
原始碼就在那裡,不是嗎?
是的,原始碼就在那。它是LLVM裡compiler-rt
專案的一部分。梳理程式碼後我發現了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
的第三個和第四個值有點奇怪。它們看起來應該是函式指標,但是我們編譯階段看到的是兩個字串。暫時先忽略它們。
Block_layout
的isa
很有趣,它一定就是_NSConcreteGlobalBlock
,而且一定是block視作一個一個Objective-C
物件的原因。如果_NSConcreteGlobalBlock
是一個類,那麼OC的訊息分發機制一定樂於把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);
}複製程式碼
太好了,現在我們已經更多地瞭解了block底層是如何工作的。