(譯)窺探Blocks (1)

本文翻譯自Matt Galloway的博客,藉此機會學習一下Block的內部原理。html

今天咱們從編譯器的視角來研究一下Block的內部是怎麼工做的。這裏說的Blocks指的是Apple爲C語言添加的閉包,並且如今從clang/LLVM角度來講已經成爲了語言的一部分。我一直很好奇Block究竟是什麼以及怎樣被視爲一個Objective-C對象的(你能夠對它們執行copyretainrelease操做。)這篇博客來稍微研究一下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是如何被建立以及如何被調用的。若是二者都放在一個方法裏面,編譯優化器可能比較聰明,那咱們就看不到有趣的現象了。我必須聲明runBlocknoinline的,不然優化器會把它內聯到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。寄存器r0ARM 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_globalBlock_descriptor結構體就是___block_descriptor_tmp。並且,我猜對了descriptor的第二個值就是size。Block_descriptor的第三個和第四個值有點奇怪。它們看起來應該是函數指針,可是咱們編譯階段看到的是兩個字符串。暫時先忽略它們。

Block_layoutisa頗有趣,它必定就是_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底層是如何工做的。

相關文章
相關標籤/搜索