##block初体验
定义和使用block很简单:
1 | int main(int argc, const char * argv[]) { |
但是作为一个有理想的programmer,不仅要知道其怎么用,还要知道其内在原理。
##揭开面纱
首先,将定义了上述block的main.m函数用clang 转换为main.cpp:
1 | clang -rewrite-objc main.m |
在同目录下会生成一个同名的.cpp文件。
打开cpp文件,找到main函数:
1 | int main(int argc, const char * argv[]) { |
为了方便理解,先用typedef将其简化:
1 | typedef void(*SL_BLOCK_TYPE)(int); |
先看第一句:
1 | SL_BLOCK_TYPE sl_block = SL_BLOCK_TYPE &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA); |
找到其中的未知函数或者变量:__main_block_impl_0、__main_block_func_0、__main_block_desc_0_DATA,在文件中找到其定义。
先看__main_block_impl_0:
1 | struct __main_block_impl_0 { |
可以看出,他是一个结构体,而上述__main_block_impl_0((void *, void *)是调用了其构造函数,返回一个__main_block_impl_0类型的结构体,并对该结构体取地址作为变量sl_block的值。
继续研究__main_block_impl_0结构体的内部定义,发现其包含另外两个结构体:
1 | struct __block_impl impl; |
先看第一个,找到其具体定义:
1 | struct __block_impl { |
这个好理解,就是简单的结构体,里面包含了两个int型数据和两个指针。为了理解,不在类型之间跳来跳去,直接__block_impl结构体消除,即将__main_block_impl_0结构体定义为:
1 | struct __main_block_impl_0 { |
然后看__main_block_desc_0:
1 | static struct __main_block_desc_0 { |
这里不仅定义了一个结构体,还定义并初始化了一个全局变量__main_block_desc_0_DATA。
因为这个结构体保存的信息为一个保留位reserved和block的size,就不做细研究。
__main_block_impl_0结构体解析完了,回到刚刚的位置,三个位置变量,两个(__main_block_impl_0、__main_block_desc_0_DATA)已经知道是什么了,还剩下一个__main_block_func_0。在文件中找到这个关键字的定义:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a1) { |
很明显可以看出,这是一个函数,更确切的说,这就是block的实现部分。在sl_block的定义和赋值中可以看出,这个函数被当成参数传递给了及结构体__main_block_impl_0的构造函数,而结构体的构造函数中,又将该函数指针,赋值给了其成员变量FuncPtr。
到这里,基本就很明朗了:
定义一个block,实际上是定义了一个结构体,而block中要执行的代码,则被定义为一个函数,并且该函数的函数指针被结构体的FuncPtr成员所持有。
最后,看看block是怎么被调用的吧:
1 | (FuncPtr_TYPE((__block_impl *)sl_block)->FuncPtr)((__block_impl *)sl_block, 12); |
这个应该很容易就看出来了,就是函数指针的调用。取sl_block结构体中的FuncPtr成员,然后调用。
##深入研究—-捕捉外部变量
我们都知道,在block中,我们可以使用一些外部变量,那么,这就有一个问题了,block是怎么捕捉外部变量的呢?
将之前的main.m修改一下,再rewrite成cpp,细究。
main.m
1 | int main(int argc, const char * argv[]) { |
main.cpp
1 | int main(int argc, const char * argv[]) { |
可以看出,__main_block_impl_0构造函数的参数发生了变化,传递了两个参数:i_a,、i_b。
看看其结构体内部变化:
1 | struct __main_block_impl_0 { |
结构体多了两个和外部变量同名同类型的成员变量。
再看其实现部分:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a1) { |
可以看出,block内部使用的实际上不是外部变量,而是其成员变量,即外部变量的副本(bound by copy)。
综上所述,可以得出,block对于用到的外部变量,会将其保存为成员变量,以供block内部使用。
##__block关键字
既然说到block对于外部变量的捕捉,就不得不提__block关键字。
对于上述的例子,外部变量对于block是readonly的。那么怎么才能readwrite呢?apple在设计block的时候肯定是早就考虑到了这个的,__block就是为了解决外部变量的write问题。
######ps:在C语言的角度考虑这个问题
其实block的实现,基本都是基于c和c++的,所以考虑这个问题,很自然就想到了c语言的值传递和地址传递。(有兴趣可自行研究)
######apple的实现
修改main.m,并rewrite成cpp:
1 | int main(int argc, const char * argv[]) { |
1 | int main(int argc, const char * argv[]) { |
对比之前的,发现i_a变量,变成了一个结构体__Block_byref_i_a_0:
1 | struct __Block_byref_i_a_0 { |
而且在传参的时候,也是变成了地址传递((__Block_byref_i_a_0 *)&i_a,)。这就为修改i_a的值奠定了基础。因为地址传参之后修改的肯定不再是副本,而是本身。
再看看实现部分:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a1) { |
到这里基本就明了了,__block修饰的变量,传递方式变成了地址传递,也就可以达到readwrite的效果了~最后,(i_a->__forwarding->i_a)= i_b + (i_a->__forwarding->i_a);这个骚操作没搞懂,没有搞错的话,i_a和i_a->__forwarding是同一个地址。
__forwarding
指向自身的操作,是因为block有可能存放在栈上,也有可能存放在堆上,当block没有发生复制时,它指向栈上的对象,当block发生复制时,block指向堆上的对象。