(译)窥探Blocks (1)

本文翻译自Matt Galloway的博客,借此机会学习一下Block的内部原理。

今天我们从编译器的视角来研究一下Block的内部是怎么工作的。这里说的Blocks指的是Apple为C语言添加的闭包,而且现在从clang/LLVM角度来说已经成为了语言的一部分。我一直很好奇Block到底是什么以及怎样被视为一个Objective-C对象的(你可以对它们执行copyretainrelease操作。)这篇博客来稍微研究一下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是如何被创建以及如何被调用的。如果两者都放在一个方法里面,编译优化器可能比较聪明,那我们就看不到有趣的现象了。我必须声明runBlocknoinline的,否则优化器会把它内联到doBlock方法中,这会导致上述同样的问题。

上述代码编译出来的汇编代码如下(编译器是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底层是如何工作的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 原文地址:http://www.galloway.me.uk/2012/10/a-look-inside-bloc...
    tongxyj阅读 1,324评论 0 2
  • 前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这...
    小人不才阅读 3,729评论 0 23
  • 本文翻译自Matt Galloway的博客 之前的文章(译)窥探Blocks(1)我们已经了解了block的内部原...
    foolishBoy阅读 1,080评论 0 2
  • Blocks Blocks Blocks 是带有局部变量的匿名函数 截取自动变量值 int main(){ ...
    南京小伙阅读 882评论 1 3
  • 花了一段时间对Block深入的研究了一下,以下是我边研究边写的笔记记录,其中大部分内容都是从多线程和内存管理那本书...
    doudo阅读 133评论 0 0