原文作者:Matt Galloway
今天,我从编译器的角度研究了一下blocks是如何工作的。我所说的blocks是苹果公司在C语言中添加的闭包,如今从clang/LLVM的角度来看,blocks确实是语言的一部分。我一直都很好奇”block”是如何运作的,”block”是如何是如何神奇的作为Objective-C对象出现的(例如:你可以对block对象执行copy
、retain
、release
操作)。
基础
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是如何被设置和调用的。如何设置和调用的代码写在一个方法中,编译器很聪明,以至于会把我们想看到的细节给优化掉。因为我写了一个noinline
的方法runBlockA
,所以编译器在doBlockA
中就不会内联这个方法,把两个方法优化为一个方法。
该代码的相关位被编译成如下(armv7, 03
):
.globl _runBlockA
.align 2
.code 16 @ @runBlockA
.thumb_func _runBlockA
_runBlockA:
@ BB#0:
ldr r1, [r0, #12]
bx r1
复制代码
这就是编译后的runBlockA
方法的指令集。所以,这很简单。回顾一下这个方法的源代码,这个方法只是调用了一下block。在ARM的EABI中,r0
(寄存器r0)被设置为方法的第一个参数。因此,第一个指令意味着存在r0+12
这块地址中的值被加载到r1
中。可以把这个看做对指针的解引用,向其读入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
复制代码
好吧,这也很简单。这是一个程序计数器的相关加载。你可以把这当做是把__block_literal_gobal
的变量的地址加载进r0
。然后runBlockA
方法就被调用了。我们可以看出,被传递到runBlockA
方法中的block对象就是以上汇编指令集中的__block_literal_gobal
。
现在我们有些进展了。但是__block_literal_gobal
到底是什么呢?我们通过汇编指令集发现如下:
.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)。这个结构体一定是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_global
,Block_descriptor
结构体就是我们分析的___block_descriptor_tmp
。我猜测的描述符中的第二个值是size的观点是对的。奇怪的是Block_descriptor
中的第三和第四个值。这两个值看起来应该是函数的指针,但是在我们编译后的指令集中这两个值是两个字符串。这两个值我们暂且按下不表。
Block_layout
中的isa
很值得关注,因为他可能就是_NSConcreteGlobalBlock
。而且也可能是一个block如何可以具有一个Objective-C对象行为的关键。如果_NSConcreteGlobalBlock
是一个类
,那么Objective-C的消息传递机制系统很乐意将一个block对象当做一个普通对象来处理。这与无缝桥接(toll-free bridging)工作机制很相似。关于这方面(toll-free bridging)的更多信息,请阅读Mike Ash’s的优秀博文。
将上面的零碎的点合在一起,编译器好像是这样处理代码的:
#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下运作的细节就很好理解了。
下一步
接下来,我会继续去探究带有参数的block是如何从作用域捕获变量的。这肯定会有所不同,请持续关注!