iOS HackeriOS Developer

Block探究:第一篇(Global_Block)

2017-09-15  本文已影响68人  tongxyj

原文地址:http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-1/
如原作者发现有侵权行为可责令我在24小时之内删除,前提是你能看到。

我最近一直在研究block在编译器层面的内部实现原理。block相当于苹果为C语言也赋予了闭包的特性,并且现在clang/LLVM编译器已经可以完全支持它。我过去一直在想,“block”到底是什么,它为什么如此神奇,几乎和OC的对象一样(你可以对它进行copy, retain, release操作)。让我们在这篇博客中探索一下block。

基础

这样定义一个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是如何被创建和调用的。如果block的创建和调用都在一个函数里面,那么编译器会对它进行优化,那样我们就看不到让我们感兴趣的结果了,我给runBlockA函数加上noinline特性,告诉编译器不要将函数runBlockA内联到doBlockA中,也是为了避免编译器优化而导致同样的结果。(这个地方我对于inlinenoinline理解的不是很好,所以找到了这篇关于 inline函数的文章)

相关代码在armv7架构中编译的结果如下:

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

上面是runBlockA函数的编译结果,看起来很简单,参照上面的代码,runBlockA这个函数只是简单的调用了block。寄存器r0被设置为ARM EABI函数的第一个参数。ldr r1, [r0, #12]这句指令的意思是将存储器地址为r0+12字节数据读入寄存器r0。把它看成是一个指针的解引用,从r0的地址出开始读取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

这部分也很简单,是有关于pc(program counter)程序指令寄存器相关的加载,你可以把他理解成从变量名为___block_literal_global的地址中取出值并存放在r0里,然后调用runBlockA函数。从之前的代码中我们知道runBlockA函数有一个参数block,所以这个___block_literal_global就是那个参数。

这不正是我们要找的东西!那这个___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个字节(因为一个long类型在32位机器上占4个字节),这个结构体肯定就是runBlockA函数中调用的那个block。其中第12字节的地方,___doBlockA_block_invoke_0这个名字看起来更像是一个函数指针,这也是runBlockA函数中跳转执行的那个地址(在runblockA函数中pc读取的是r0开始偏移12个字节的地址中的值放入r1,然后用bx指令执行跳转到r1所存储的地址处,从那里开始执行)。

那么__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      //(跳转到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真正的实现,因为我们之前给BlockA赋值了一个空的block,所以会立即返回(bx lr)。

接着是___block_descriptor_tmp,这好像是另外一个独立的结构,它包含4个值,第二个值20代表了___block_literal_global的大小。接着是一个名为.str的C字符串,它的值为v4@?0,看起来有点像某个类型的编码形式。这可能是block 类型的编码(也就是返回void和不带参数的类型)。还有一个值我暂时也不知道是干啥的。


源码在这里不是吗?

是的,源码就在这里!下面的代码是LLVMcompiler-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。我之前猜对了Block_descriptor中的第二个值代表的是___block_literal_global的大小,但第三个和第四个值有点奇怪,它们应该是两个函数指针,但在编译后的代码中它们更像是两个字符串。我暂且先忽略这点。

Block_layout结构体的isa一定就是_NSConcreteGlobalBlock,这也是一个block可以模仿Objective-C对象操作的关键。如果_NSConcreteGlobalBlock是一个类(Class),那么Objective-C的消息派发机制理应将block也当做一个正常的对象来看待了。这和toll-free bridging的工作原理类似。想了解更多信息请看Mike Ash的一篇非常屌的有关Toll Free Bridging的blog

综上所述(Having pieced all that together,以后写英语作文可以在最后一段用这个开头,感觉屌屌的),编译器更像是按下面这样编译的(这张图可能是全文的重点,所以我把原版彩图放上来了):


block

通过上面的了解现在是不是更容易理解block的本质了。


下一篇讲啥?

下一篇我将会讲带一个参数的block以及block是如何捕获外部变量的,这就有点难了!期待我的下一篇文章吧。

P.S.本文作者就是《Effective Objective-C 2.0》的作者,这篇文章写的时间比较早,但很多底层的知识我觉得应该是不会变的,中间有很多设计ARM汇编的东西之前没有接触过,所以理解的不是很好所以翻译不出其中的精髓,能看到的望大家多多指出问题,希望对大家有所帮助,大家加油。

上一篇下一篇

猜你喜欢

热点阅读