原文地址: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
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中,也是为了避免编译器优化而导致同样的结果。(这个地方我对于inline和noinline理解的不是很好,所以找到了这篇关于 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和不带参数的类型)。还有一个值我暂时也不知道是干啥的。
源码在这里不是吗?
是的,源码就在这里!下面的代码是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_global,Block_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是如何捕获外部变量的,这就有点难了!期待我的下一篇文章吧。
P.S.本文作者就是《Effective Objective-C 2.0》的作者,这篇文章写的时间比较早,但很多底层的知识我觉得应该是不会变的,中间有很多设计ARM汇编的东西之前没有接触过,所以理解的不是很好所以翻译不出其中的精髓,能看到的望大家多多指出问题,希望对大家有所帮助,大家加油。