(译)窥探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底层是如何工做的。

相关文章
相关标签/搜索