iOS中的block  block是什么,block的本质

block 是c语言的扩充功能,是带有自动变量(局部变量)的匿名函数,也称之为 闭包。

先说结论,block中包含了一个isa指针,所以它是一个对象,一个包含了函数调用和函数调用环境的OC对象(那么是swift对象吗?感兴趣的,可以自己动手研究下)。

1.转换代码,分析结构

以下是过程:①写个简单的block 

swift block 回调 swift block原理_构造函数

② 使用命令 clang -rewrite-objc hello.m 转为c++

swift block 回调 swift block原理_block_02

互相对比可以看出,我们对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

swift block 回调 swift block原理_block_03

可以看出,在__main_block_impl_0中,包含了一个3个变量,__block_impl,__main_block_desc_0,classID 以及一个同名构造函数。还是一个个来看。

 

最先看出的是 int classID,对比代码,发现这个就是我们在block实现中,捕获的局部变量classID。这里也就明白了,block的拘捕变量捕获的机制,同时也是为什么下面这样写,在block中classID的值不会改变的原因,

swift block 回调 swift block原理_swift block 回调_04

因为,在执行到初始化的时候,已经进行了变量捕获。

flags则是直接给了一个默认值0,所以我们只需要搞明白fp,desc。

1.2 _block_impl

那么,还剩余两个结构体__block_impl,__main_block_desc_0。 我们先去看下__block_impl。(先盲猜一下,我看到了imp。)

swift block 回调 swift block原理_构造函数_05

看到了isa, 所以可以推断,block本质是一个oc对象。 看到了funcPtr,推测应该是存储我们在block中的实现。接下来回头看下__main_block_impl_0的构造函数。

swift block 回调 swift block原理_swift block 回调_06

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

swift block 回调 swift block原理_赋值_07

swift block 回调 swift block原理_swift block 回调_08

左边是__main_block_func_0  右边是我们block中的实现,所以__main_block_func_0 是什么,不言而喻。也证实了我们上面的推断,funcPtr,是存储我们在block中的实现。

同时,这里也对classID做了一个取值,以用于下面函数的使用。

那么现在我们还剩下一个 __main_block_desc_0

1.4__main_block_desc_0

swift block 回调 swift block原理_ios_09

可以看出,这个结构体相对简单一点,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

swift block 回调 swift block原理_构造函数_10

swift block 回调 swift block原理_swift block 回调_11

swift block 回调 swift block原理_ios_12

通过对比,我们可以看出,调用block的首地址,和funcPtr是保持一致的,所以可以佐证,我们上面的分析正确。

 

而如果,我们推断错的话,举个例子,如下图

swift block 回调 swift block原理_构造函数_13

写了一个错误的struct,结果可以看到,转换出来的效果,是很奇怪的。 可以注意到的是,虽然错误了,但是错误Desc的值,和正确isa的值是一致的,而根据上面的分析,我们不难发现,正常的block的Struct中,_block_impl是首位变量,而isa是_block_impl的首位变量,所以导致了,本应该赋值给isa的值,错误的赋值给了desc。佐证了,struct的强制转换机制。是直接首位开始覆盖。

 

后续会,一起学习下,block中分类,和block中的变量捕获机制

如有错误,欢迎指出。