Objc 中 Block 的实现原理

Posted by Abin's blog on July 12, 2017

Objc 中的 Block 其实是一个对象,之前也说过 Objc 中对象的结构

来看下 Block 的实现,新建 TooT.m 文件写一个 func_TooT 函数:

void func_TooT(void)
{
    int tt_value = 0;
    void (^bt)(void);
    bt = ^(){
        printf("%d",tt_value);
    };
    bt();
}

然后在终端中执行 clang -rewrite-objc TooT.m 查看 C++ 的实现。

打开 TooT.cpp 文件,直接搜索 func_TooT,得到以上代码的实现:

struct __func_TooT_block_impl_0 {
  struct __block_impl impl;
  struct __func_TooT_block_desc_0* Desc;
  int tt_value;
  __func_TooT_block_impl_0(void *fp, struct __func_TooT_block_desc_0 *desc, int _tt_value, int flags=0) : tt_value(_tt_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __func_TooT_block_func_0(struct __func_TooT_block_impl_0 *__cself) {
  int tt_value = __cself->tt_value; // bound by copy

        printf("%d",tt_value);
    }

static struct __func_TooT_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __func_TooT_block_desc_0_DATA = { 0, sizeof(struct __func_TooT_block_impl_0)};
void func_TooT(void)
{
    int tt_value = 0;
    void (*bt)(void);
    bt = ((void (*)())&__func_TooT_block_impl_0((void *)__func_TooT_block_func_0, &__func_TooT_block_desc_0_DATA, tt_value));
    ((void (*)(__block_impl *))((__block_impl *)bt)->FuncPtr)((__block_impl *)bt);
}

以上内容其实是四部分:

  1. 该 Block 的结构(数据)
  2. 该 Block 的执行的动作
  3. 该 Block 的信息,比如所占空间大小
  4. func_TooT 函数

Block 的结构

从中可以看到 Block 被编译器重写为以下内容:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
struct __func_TooT_block_impl_0 {
  struct __block_impl impl;
  struct __func_TooT_block_desc_0* Desc;
  int tt_value;
  __func_TooT_block_impl_0(void *fp, struct __func_TooT_block_desc_0 *desc, int _tt_value, int flags=0) : tt_value(_tt_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

不难看出,一个完整的 Block 有以下 6 部分:

  1. isa:指向类型地址,实现对象的功能
  2. Flags:
  3. Reserved:保留变量
  4. FuncPtr:Block 要执行的动作函数地址
  5. Desc:Block 的描述信息
  6. Variables:Block 引用外部的同名同类型变量,在内部拷贝了一份;可能有多个

Block 的动作

Block 要执行的动作保存在 __func_TooT_block_func_0 函数中,Block 自身会保存该函数的地址,执行的时候将 Block 自身作为参数传进来,类似于 Objc 中对象的方法默认第一个参数就是对象本身 self。 在 Block 中取出被 Block 捕获拷贝的变量,执行在 Block 定义的行为。

Block 的描述

只有两个字段: reservedBlock_size,保留字段和 Block 的size

Block 的初始化

第四部分就是 void func_TooT(void) ,Block 被初始化去掉类型强转可以被简化为:

bt = &__func_TooT_block_impl_0(__func_TooT_block_func_0, &__func_TooT_block_desc_0_DATA, tt_value));

其中三个参数分别为 Block 动作函数地址,附加信息 和 所捕获的变量。 其中 动作函数地址 就是上面所说 Block 要执行的代码,附加信息默认是 {0, sizeof(struct block)},所捕获的变量在这里只是一个,如果捕获多个时就会有多个参数。

总结

Block 也是一个对象,除了有指向自身类型的 isa 指针,本身还有一个函数指针指向自身要执行动作的函数地址,该函数以 Block 本身为参数,Block 本身还会存储一份引用外部变量的同名同类型的变量(可能为多个),这样才执行的时候可以获取到所引用变量的值。

__block 的实现原理

上面说过 Block 捕获变量是以形参的形式传进一个函数被 Block 本身所持有,这也是在 Objc 中值类型只能被 Block 捕获读取,却不能修改,但是给变量加上 __block 修饰符就可以,看下编译器是怎么实现的,来段代码:

void func_TooT(void)
{
    __block int tt_value = 1021;
    void (^bt)(void);
    bt = ^(){
        tt_value++;
    };
    bt();
}

clang -rewrite-objc TooT.m 看 C++ 代码发现:

struct __Block_byref_tt_value_0 {
  void *__isa;
__Block_byref_tt_value_0 *__forwarding;
 int __flags;
 int __size;
 int tt_value;
};
static void __func_TooT_block_func_0(struct __func_TooT_block_impl_0 *__cself) {
  __Block_byref_tt_value_0 *tt_value = __cself->tt_value; // bound by ref

        (tt_value->__forwarding->tt_value)++;
    }
void func_TooT(void)
{
    __attribute__((__blocks__(byref))) __Block_byref_tt_value_0 tt_value = {(void*)0,(__Block_byref_tt_value_0 *)&tt_value, 0, sizeof(__Block_byref_tt_value_0), 1021};
    void (*bt)(void);
    bt = ((void (*)())&__func_TooT_block_impl_0((void *)__func_TooT_block_func_0, &__func_TooT_block_desc_0_DATA, (__Block_byref_tt_value_0 *)&tt_value, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)bt)->FuncPtr)((__block_impl *)bt);
}

可以看到, 相对于上面的代码,只多了 __block 修饰词,int 类型就被编译器重写为一个结构体,结构体中的属性中有一个 __forwarding 指针,还有一个同名同类型变量。

看初始化结构体的时候把自身地址赋值给 __forwarding,这样即便结构体被当做形参传给 Block,Block 也是可以通过结构体的 __forwarding 指针指向的结构体的同名同类型变量取到真正的变量,只能说这个思路实现的真好~

Block 的动作函数中我们也可以看到:

// tt_value++; 被重写为:
(tt_value->__forwarding->tt_value)++;