本文翻译自Matt Galloway的博客,借此机会学习一下Block的内部原理。
今天我们从编译器的视角来研究一下Block的内部是怎么工作的。这里说的Blocks指的是Apple为C语言添加的闭包,而且现在从clang/LLVM角度来说已经成为了语言的一部分。我一直很好奇Block到底是什么以及怎样被视为一个 Objective-C
对象的(你可以对它们执行copy
,retain
,release
操作。)这篇博客来稍微研究一下Block。
基础
下面代码是一个Block:
void(^block)(void) = ^{
NSLog(@"I'm a block!");
};
它创建了一个叫做block
的变量,而且用一个简单的代码块赋值给它。这很简单。这就完成了?不,我想了解编译器为这一小段代码干了什么事。
此外,你也可以给block传递一个参数:
void(^block)(int a) = ^{
NSLog(@"I'm a block! a = %i", a);
};
甚至还可以反悔一个值:
int(^block)(void) = ^{
NSLog(@"I'm a block!");
return 1;
};
作为一个闭包,它们捕获了它们的上下文:
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是如何被创建以及如何被调用的。如果两者都放在一个方法里面,编译优化器可能比较聪明,那我们就看不到有趣的现象了。我必须声明runBlock
为noinline
的,否则优化器会把它内联到doBlock
方法中,这会导致上述同样的问题。
上述代码编译出来的汇编代码如下(编译器是armv7,03):
.globl _runBlockA
.align 2
.code 16 @ @runBlockA
.thumb_func _runBlockA
_runBlockA:
@ BB#0:
ldr r1, [r0, #12]
bx r1
这是runBlockA
部分,非常的简单。回顾一下源码,这个方法仅仅调用了一个block。寄存器r0
在ARM EABI中被设置为这个方法的第一个参数。因此第一条指令意味着r1
是从r0 + 12
的地址处加载的。可以认为这是一个指针的间接引用,读入12个字节进去。然后我们跳转到哪个地址。注意使用的是r1
,意味着r0
仍然是参数block自身。所以这看起来就像是正在调用的方法把这个block作为第一个参数。
从这里我可以确定block很可能是一些结构体组成,实际执行的方法是存储在相应结构体里面的12个字节。当传递一个block时,实际上传递的是指向某一个结构体的指针。
现在来看看doBlock
方法:
.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_global
的地址加载到r0
。然后调用了_runBlockA
方法。我们已经知道r0
当作block对象被传递给_runBlockA
了,那___block_literal_global
一定就是那个block对象。
现在我们已经取得一些进展了!但是___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字节大小。这肯定就是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
。而且,我猜对了descriptor的第二个值就是size。Block_descriptor
的第三个和第四个值有点奇怪。它们看起来应该是函数指针,但是我们编译阶段看到的是两个字符串。暂时先忽略它们。
Block_layout
的isa
很有趣,它一定就是_NSConcreteGlobalBlock
,而且一定是block视作一个一个Objective-C
对象的原因。如果_NSConcreteGlobalBlock
是一个类,那么OC的消息分发机制一定乐于把block当作一个普通的对象。这类似于toll-free bridging的工作原理。
总结起来,编译器好像用如下的逻辑来处理代码:
#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底层是如何工作的。