原文地址:http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-1/
如原作者发现有侵权行为可责令我在24小时之内删除,前提是你能看到。
这是我从编译器角度了解block内部实现的第二篇文章。在这篇文章里,我将会研究一下block在栈上的几种类型。
Block类型
在第一篇文章里我们了解到,block中有一个isa指针指向了_NSConcreteGlobalBlock这个类,因为block结构体和block中的descriptor结构体在编译时就全部初始化完成了,所以我们可以看到里面的变量。block有几种不同的类型,每种类型都有与其关联的类,但是我们只需要考虑他们中常见的三种即可:
1._NSConcreteGlobalBlock是一种在编译期间就完全定义好的全局block,这类block没有捕获任何外部变量,例如一个空的block。
2._NSConcreteStackBlock是一种位于栈上的block,这种block在最终被copy到堆中之前一直都存储在栈上。
3._NSConcreteMallocBlock是一种位于堆上的block,在对block进行copy操作之后,它的引用计数会增加,知道引用计数减为0,这个block就会被释放。
有捕获范围的block
来看看下面的代码:
名为foo的函数有一个参数,在block中调用foo函数的时候将外面的变量a捕获并传给函数,同样的,我们来看看在armv7架构下汇编的过程,相关代码如下:
这段汇编代码和第一篇中的一样,调用了block的invoke函数,接下来看看doBlockA函数:
这段和之前的比起来有点难度了,和之前的加载全局block不同,这里做了更多的事,虽然看起来有点恐怖,但认真看其实很容易看到它做了些什么。因为编译器没有对这些指令进行优化,所以我将要对这些代码在不改变原有功能的基础上重新整理一下,下面是整理后的代码:
主要的功能点如下:
1.一开始让r7入栈是为了避免r7的内容被覆盖,因为r7里面的内容在函数间调用时需要用到。lr(链接寄存器)保存了函数返回时要执行的下一条指令的地址。最后将栈指针保存在r7中。
2.从栈顶指针减去24个字节,也就是从栈中开辟出24字节用来存储数据。
3.这里的代码是为了对符号L__NSConcreteStackBlock$non_lazy_ptr进行寻址,跟pc有关,当代码最终被链接时不管这段代码在二进制文件中的什么位置计算机都能通过pc找到它,并将它存储在栈指针所指的内存地址中。
4.值1073741824被存储在栈顶指针+4的位置。
5.将0存储到栈指针 + 8 的位置。现在,将要发生什么可能已经变得逐渐清晰了——在栈上创建了一个Block_layout结构!到现在为止,已经设置了该结构的3个值:isa指针,flags和reserved值。
6.___doBlockA_block_invoke_0存储在栈指针+12的地址处,也就是block结构体的invoke属性。
7.___block_descriptor_tmp存储在栈指针+16的地址处,也就是block结构体的descriptor属性。
8.值128存储在栈指针+20的地址处,你可能发现了Block_layout结构体中只有5个值,并且这5个值已经都存储在栈中了,那么接下来栈中要存储什么?你会发现正是从外面捕获到的值128。所以这肯定就是存储block使用到的值的地方——在Block_layout结构尾部。
9.现在栈指针指向的是一个完整的block机构体,然后将栈指针存入r0中,然后调用runBlockA函数。(注意:在ARM EABI中r0通常用来存放函数的第一个参数)。
10.最后,让栈指针+24收回之前在栈中开辟的24字节的空间,接着将栈中的内容分别pop到r7和pc中,pop回r7的就是函数一开始从r7push到栈中的内容,pc的值是函数开始时push到栈中的lr的值,这样在函数返回时程序可以继续执行下面的指令。
哇,如果你看到这的话,说明你太棒了!
最后一部分让俺们来看看block中的invoke函数和descriptor编译的结果。我希望他们要比第一篇中的global block的这两个属性简单。看代码:
的确和第一篇中的没有什么差别,唯一不同的是descriptor中的size值的大小不同,现在是24而不是20,这是因为block捕获到一个整数所以block的大小变为了24,之前也看到在block创建的时候有额外的4个字节在block的尾部一起被存入了栈中。
在实际的block调用函数里,比如__doBlockA_block_invoke_0,我们能看到在调用___doBlockA_block_invoke_0函数中先从r0 + 20地址初开始读取了4个字节的数据到r0中,这额外的4个字节也就是block从外部捕获到那个整数,然后调用foo函数。
如果捕获的是一个对象类型呢?
我们需要考虑的下一个问题是如果block捕获的外部变量是一个对象类型比如NSString而不是一个整数,那么会发生什么,看看下面的代码:
doBlockA函数我就不讲了,和之前的一样,没有太多的变化。有点意思的是这个block的descriptor结构体指针:
注意这边有两个函数指针:__copy_helper_block和__destroy_helper_block。下面是这些函数的定义:
我假设block被拷贝和销毁正是因为有这两个函数,那么他们一定会对block捕获的对象进行retain和release操作。拷贝函数接收两个参数(r0和r1),而销毁函数接收一个参数。可以看出所有的拷贝和销毁任务都应该是由__Block_object_assign和__Block_object_dispose两个函数完成的。这两个函数位于block的运行时代码中,是LLVM里面compiler-rt工程的一部分。
如果你想继续了解runtime中有关block的代码,可以去http://compiler-rt.llvm.org下载相关源代码。尤其要去看看runtime.c这个文件。
下一篇讲啥?
下一篇准备深入了解一下block在runtime中的Block_copy,看看它是如何工作的。这也能让我们能更好的理解上面在block捕获的外部对象时创建的__copy_helper_block和__destroy_helper_block函数。