iOS中的block block是什么,block的本质
block 是c语言的扩充功能,是带有自动变量(局部变量)的匿名函数,也称之为 闭包。
先说结论,block中包含了一个isa指针,所以它是一个对象,一个包含了函数调用和函数调用环境的OC对象(那么是swift对象吗?感兴趣的,可以自己动手研究下)。
1.转换代码,分析结构
以下是过程:①写个简单的block
② 使用命令 clang -rewrite-objc hello.m 转为c++
互相对比可以看出,我们对block的实现,转变成了
((void (*)(NSString *))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, classID))
接下来,我们来分析下,这行代码是什么意思。
首先进行一个代码拆分,方便阅读。
//等价于 ^(NSString *str_test)
((void (*)(NSString *))
//构造 结构体__main_block_impl_0 并将地址赋值给block
& __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, classID))
所以,可以得出第一个结论:block 是调用了一个 __main_block_impl_0结构体,__main_block_impl_0将地址赋值给了block。 所以我们可以确定 block 是一个结构体,那么接下来,看一下,这个__main_block_impl_0 是什么。
1.1 __main_block_impl_0
可以看出,在__main_block_impl_0中,包含了一个3个变量,__block_impl,__main_block_desc_0,classID 以及一个同名构造函数。还是一个个来看。
最先看出的是 int classID,对比代码,发现这个就是我们在block实现中,捕获的局部变量classID。这里也就明白了,block的拘捕变量捕获的机制,同时也是为什么下面这样写,在block中classID的值不会改变的原因,
因为,在执行到初始化的时候,已经进行了变量捕获。
flags则是直接给了一个默认值0,所以我们只需要搞明白fp,desc。
1.2 _block_impl
那么,还剩余两个结构体__block_impl,__main_block_desc_0。 我们先去看下__block_impl。(先盲猜一下,我看到了imp。)
看到了isa, 所以可以推断,block本质是一个oc对象。 看到了funcPtr,推测应该是存储我们在block中的实现。接下来回头看下__main_block_impl_0的构造函数。
isa 指向了_NSConcreteStackBlock。暂时可以理解为block 是_NSConcreteStackBlock类的(block的具体是属于什么类,后续会写到,这里可以先用来暂时理解)
impl.FuncPtr = fp;
这个我们可以看出funcPtr是一个叫fp的函数指针,那么 我们具体看一下fp。 在调用__main_block_impl_0的构造函数中,fp指向了 __main_block_func_0。那么我们去看下__main_block_func_0是什么。
1.3__main_block_func_0
左边是__main_block_func_0 右边是我们block中的实现,所以__main_block_func_0 是什么,不言而喻。也证实了我们上面的推断,funcPtr,是存储我们在block中的实现。
同时,这里也对classID做了一个取值,以用于下面函数的使用。
那么现在我们还剩下一个 __main_block_desc_0
1.4__main_block_desc_0
可以看出,这个结构体相对简单一点,reserved 直接0, Block_size 计算__main_block_impl_0的内存大小。到这里,我们可以总结下,block的结构了
1.5总结block的结构
struct __main_block_impl_0 {
//impl其中存储了,block的isa, 我们在block中的实现以被封装成__main_block_func_0,被赋值给impl的funcPtr
struct __block_impl impl;
//Desc 主要存储了内存的占用大小
struct __main_block_desc_0* Desc;
//捕获到的变量
int classID;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _classID, int flags=0) : classID(_classID) {
//impl进行赋值
impl.isa = &_NSConcreteStackBlock;//isa
impl.Flags = flags;
impl.FuncPtr = fp;//实现函数
Desc = desc;
}
};
2.分析block的调用
转换后,我们调用的代码变为
((void (*)(__block_impl *, NSString *))((__block_impl *)blocktest)->FuncPtr)((__block_impl *)blocktest, (NSString *)&__NSConstantStringImpl__var_folders____0h8s87296p7d2d0_3s0nnkxh0000gn_T_hello_6233d9_mi_2);
看起来不大方便阅读,整理一下
((void (*)(__block_impl *, NSString *))//有这样一个block
((__block_impl *)blocktest)->FuncPtr)//block变量名为blocktest 取出它的funcPtr
((__block_impl *)blocktest, (NSString *)&__NSConstantStringImpl__var_folders____0h8s87296p7d2d0_3s0nnkxh0000gn_T_hello_6233d9_mi_2);//调用它的funcPtr
那么。这样整理下,就很容易阅读了。但是会发现一个问题,我们上面分析时,block是 __main_block_impl_0,可是这里blocktest被强制转换为了__block_impl。之所以可以这样直接使用,是因为,在我们上面分析中,已经发现__block_impl 是 __main_block_impl_0的第一个成员,这样,__block_impl的地址,即为__main_block_impl_0的首地址,所以是可以进行转换的。这样,就成功的取到了funcPtr即取到了 __main_block_func_0。然后传入参数,执行block。
3.验证block调用
仿照上面的分析,手敲一个block的struct
通过对比,我们可以看出,调用block的首地址,和funcPtr是保持一致的,所以可以佐证,我们上面的分析正确。
而如果,我们推断错的话,举个例子,如下图
写了一个错误的struct,结果可以看到,转换出来的效果,是很奇怪的。 可以注意到的是,虽然错误了,但是错误Desc的值,和正确isa的值是一致的,而根据上面的分析,我们不难发现,正常的block的Struct中,_block_impl是首位变量,而isa是_block_impl的首位变量,所以导致了,本应该赋值给isa的值,错误的赋值给了desc。佐证了,struct的强制转换机制。是直接首位开始覆盖。
后续会,一起学习下,block中分类,和block中的变量捕获机制
如有错误,欢迎指出。