(譯)窺探Blocks (1)

本文翻譯自Matt Galloway的博客洗出,借此機會學習一下Block的內(nèi)部原理。

今天我們從編譯器的視角來研究一下Block的內(nèi)部是怎么工作的菠镇。這里說的Blocks指的是Apple為C語言添加的閉包辟犀,而且現(xiàn)在從clang/LLVM角度來說已經(jīng)成為了語言的一部分堂竟。我一直很好奇Block到底是什么以及怎樣被視為一個Objective-C對象的(你可以對它們執(zhí)行copyretain税稼,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)象了。我必須聲明runBlocknoinline的绍申,否則優(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。寄存器r0ARM 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_globalBlock_descriptor結構體就是___block_descriptor_tmp狡门。而且陷寝,我猜對了descriptor的第二個值就是size。Block_descriptor的第三個和第四個值有點奇怪其馏。它們看起來應該是函數(shù)指針凤跑,但是我們編譯階段看到的是兩個字符串。暫時先忽略它們叛复。

Block_layoutisa很有趣仔引,它一定就是_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底層是如何工作的。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夫否,一起剝皮案震驚了整個濱河市彻犁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌凰慈,老刑警劉巖汞幢,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異溉瓶,居然都是意外死亡急鳄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門堰酿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疾宏,“玉大人,你說我怎么就攤上這事触创】裁辏” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵哼绑,是天一觀的道長岩馍。 經(jīng)常有香客問我,道長抖韩,這世上最難降的妖魔是什么蛀恩? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮茂浮,結果婚禮上双谆,老公的妹妹穿的比我還像新娘。我一直安慰自己席揽,他們只是感情好顽馋,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著幌羞,像睡著了一般寸谜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上属桦,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天熊痴,我揣著相機與錄音他爸,去河邊找鬼。 笑死果善,一個胖子當著我的面吹牛讲逛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播岭埠,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蔚鸥!你這毒婦竟也來了惜论?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤止喷,失蹤者是張志新(化名)和其女友劉穎馆类,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弹谁,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡乾巧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了预愤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沟于。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖植康,靈堂內(nèi)的尸體忽然破棺而出旷太,到底是詐尸還是另有隱情,我是刑警寧澤销睁,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布供璧,位于F島的核電站,受9級特大地震影響冻记,放射性物質(zhì)發(fā)生泄漏睡毒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一冗栗、第九天 我趴在偏房一處隱蔽的房頂上張望演顾。 院中可真熱鬧,春花似錦贞瞒、人聲如沸偶房。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棕洋。三九已至,卻和暖如春乒融,著一層夾襖步出監(jiān)牢的瞬間掰盘,已是汗流浹背摄悯。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留愧捕,地道東北人奢驯。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像次绘,于是被迫代替她去往敵國和親瘪阁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 原文地址:http://www.galloway.me.uk/2012/10/a-look-inside-bloc...
    tongxyj閱讀 1,379評論 0 2
  • 前言 Blocks是C語言的擴充功能邮偎,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,768評論 0 23
  • 本文翻譯自Matt Galloway的博客 之前的文章(譯)窺探Blocks(1)我們已經(jīng)了解了block的內(nèi)部原...
    foolishBoy閱讀 1,107評論 0 2
  • Blocks Blocks Blocks 是帶有局部變量的匿名函數(shù) 截取自動變量值 int main(){ ...
    南京小伙閱讀 929評論 1 3
  • 花了一段時間對Block深入的研究了一下管跺,以下是我邊研究邊寫的筆記記錄,其中大部分內(nèi)容都是從多線程和內(nèi)存管理那本書...
    doudo閱讀 155評論 0 0