美文网首页iOS HackeriOS Developer
Block探究:第一篇(Global_Block)

Block探究:第一篇(Global_Block)

作者: tongxyj | 来源:发表于2017-09-15 15:26 被阅读68次

    原文地址: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汇编的东西之前没有接触过,所以理解的不是很好所以翻译不出其中的精髓,能看到的望大家多多指出问题,希望对大家有所帮助,大家加油。

    相关文章

      网友评论

        本文标题:Block探究:第一篇(Global_Block)

        本文链接:https://www.haomeiwen.com/subject/ssjksxtx.html