本文翻譯自Matt Galloway的博客,藉此機會學習一下Block的內部原理。html
今天咱們從編譯器的視角來研究一下Block的內部是怎麼工做的。這裏說的Blocks指的是Apple爲C語言添加的閉包,並且如今從clang/LLVM角度來講已經成爲了語言的一部分。我一直很好奇Block究竟是什麼以及怎樣被視爲一個Objective-C
對象的(你能夠對它們執行copy
,retain
,release
操做。)這篇博客來稍微研究一下Block。bash
下面代碼是一個Block:閉包
void(^block)(void) = ^{
NSLog(@"I'm a block!");
};複製代碼
它建立了一個叫作block
的變量,並且用一個簡單的代碼塊賦值給它。這很簡單。這就完成了?不,我想了解編譯器爲這一小段代碼幹了什麼事。ide
此外,你也能夠給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的,好比下例代碼:spa
#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
方法中,這會致使上述一樣的問題。.net
上述代碼編譯出來的彙編代碼以下(編譯器是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底層是如何工做的。