Block探究:第一篇(Global_Block)

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

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

推荐阅读更多精彩内容