阅读 170

【译】《A look inside blocks: Episode 1》

原文作者:Matt Galloway

原文地址:www.galloway.me.uk/2012/10/a-l…


今天,我从编译器的角度研究了一下blocks是如何工作的。我所说的blocks是苹果公司在C语言中添加的闭包,如今从clang/LLVM的角度来看,blocks确实是语言的一部分。我一直都很好奇"block"是如何运作的,"block"是如何是如何神奇的作为Objective-C对象出现的(例如:你可以对block对象执行copyretainrelease操作)。


基础

block就是下面这样的:

void(^block)(void) = ^{
    NSLog(@"I'm a block!");
};
复制代码

上面的代码创建了一个叫名为block的变量,这个变量被赋值为一个简单的block。这很简单,但这就完了吗?不!我想知道编译器对上面的代码编译的所有细节。

此外,你可以传递一个变量给block:

void(^block)(int a) = ^{
    NSLog(@"I'm a block! a = %i", a);
};
复制代码

或者从block返回一个值:

int(^block)(void) = ^{
    NSLog(@"I'm a block!");
    return 1;
};
复制代码

作为一个闭包,block捕获它所在位置的上下文:

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是如何被设置和调用的。如何设置和调用的代码写在一个方法中,编译器很聪明,以至于会把我们想看到的细节给优化掉。因为我写了一个noinline的方法runBlockA,所以编译器在doBlockA中就不会内联这个方法,把两个方法优化为一个方法。

该代码的相关位被编译成如下(armv7, 03):

.globl  _runBlockA
    .align  2
    .code   16                      @ @runBlockA
    .thumb_func     _runBlockA
_runBlockA:
@ BB#0:
    ldr     r1, [r0, #12]
    bx      r1
复制代码

这就是编译后的runBlockA方法的指令集。所以,这很简单。回顾一下这个方法的源代码,这个方法只是调用了一下block。在ARM的EABI中,r0(寄存器r0)被设置为方法的第一个参数。因此,第一个指令意味着存在r0+12这块地址中的值被加载到r1中。可以把这个看做对指针的解引用,向其读入12字节。接着我们看下r1的地址。注意,r1被使用了,这也意味着r0仍然是block本身。所以很可能这个调用的函数将block作为它的第一个参数。

我可以在这里断定,block是一种结构体,block所要调用的函数被存储在这个12字节的结构体中。当一个block被传递时,指向这些结构的一个指针被传递。

现在,看下doBlockA方法:

.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_gobal的变量的地址加载进r0。然后runBlockA方法就被调用了。我们可以看出,被传递到runBlockA方法中的block对象就是以上汇编指令集中的__block_literal_gobal

现在我们有些进展了。但是__block_literal_gobal到底是什么呢?我们通过汇编指令集发现如下:

.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字节(long)。这个结构体一定是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。我猜测的描述符中的第二个值是size的观点是对的。奇怪的是Block_descriptor中的第三和第四个值。这两个值看起来应该是函数的指针,但是在我们编译后的指令集中这两个值是两个字符串。这两个值我们暂且按下不表。

Block_layout中的isa很值得关注,因为他可能就是_NSConcreteGlobalBlock。而且也可能是一个block如何可以具有一个Objective-C对象行为的关键。如果_NSConcreteGlobalBlock是一个,那么Objective-C的消息传递机制系统很乐意将一个block对象当做一个普通对象来处理。这与无缝桥接(toll-free bridging)工作机制很相似。关于这方面(toll-free bridging)的更多信息,请阅读Mike Ash's的优秀博文

将上面的零碎的点合在一起,编译器好像是这样处理代码的:

#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下运作的细节就很好理解了。


下一步

接下来,我会继续去探究带有参数的block是如何从作用域捕获变量的。这肯定会有所不同,请持续关注!


相关系列

文章分类
iOS
文章标签