Block 是对 C 语言的扩展,给 C 语言加上了闭包(Closure)

Block 使用的语法上跟 C 语言中的函数指针非常的相似(其实现也用到了函数指针),但多出了字面量(即匿名函数)和捕获变量的能力(闭包的特性)。

如何研究 Block 的实现

使用 clang -rewrite-objc 对使用了 Block 的代码重写成 C++ 代码(实际是 C 风格的,只是用到了结构体函数),结合 Clang 文档 Block Implementation Specification 进行研究。

⚠️ clang -rewrite-objc 生成的代码和实际运行的还是不一样的,后面会说到。

匿名函数

对于下面的代码:

int main(void) {
    void (^block)(void) = ^{
        printf("Block\n");
    };
    block();
    return 0;
}

使用 clang -rewrite-objc 后,可以找到 block 变量的结构体表示:

// Block 的通用部分,每个 Block 结构体开始都是这四个变量。
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

// 结构体的命名应该是按照 __所在函数名_block_impl_第几个出现。
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) };

对其进行展开,且先忽略构造函数:

struct __main_block_impl_0 {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
  
    size_t reserved;
    size_t Block_size;
};
isa 其初始值只会是 &_NSConcreteStackBlock 或者 &_NSConcreteGlobalBlock,表示这个 block 是在栈(Stack)上或者在全局(Global)变量区(DATA 区域)。在栈上的 block 之后如果被复制到堆上的话,isa 的值会变为 &_NSConcreteMallocBlock
Flags 记录了一些标志位,记录这个 block 的一些属性。Block Implementation Specification 有介绍
Reserved 预留的位置,现在还没有使用到
FuncPtr 跟变量名所示,是一个函数指针,指向根据 block 语法写出的字面量生成的函数
reserved 也是一个预留位置,还没有用到
Block_size 这个 block 的大小

回到重写后的 main 函数:

int main(void) {
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

本来的两行 block 的创建和使用变成了两行非常复杂的玩意,首先看到 block 变量的创建:

void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

将其中的类型转换去掉:

void (*block)(void) = 
    &__main_block_impl_0(__main_block_func_0,
                         &__main_block_desc_0_DATA);

强行让 block 这个函数指针指向了一个 __main_block_impl_0 结构体。回过头来看 __main_block_impl_0 结构体的构造函数:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
}

可以看到构造函数将 FuncPtr 函数指针指向了 __main_block_func_0 函数,__main_block_func_0 函数则是 block 的字面量生成的一个普通 C 函数:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Block\n");
}

对于 block 变量的使用:

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

去掉类型转换后:

(*block->FuncPtr)(block);

可以看到就是使用了之前保存的函数指针,并传入自身作为参数(为了之后捕获变量的功能)。

捕获变量

将下面这个例子使用 clang -rewrite-objc 重写:

int main(void) {
    int value = 42;
    const char *format = "value = %d\n";
    void (^block)(void) = ^{
        printf(format, value);
    };
    block();
    return 0;
}

block 变量生成的结构体变成了这个样子:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    const char *format;
    int value;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_format, int _value, int flags=0) : format(_format), value(_value) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

可以看到结构体多出来了 formatvalue 两个成员,它们在构造函数中出现并赋值。

继续看到 block 变量声明的地方,也就是 main 函数里:

int main(void) {
    int value = 42;
    const char *format = "value = %d\n";
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, format, value));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

可以看到调用构造函数时将 formatvalue 传入了。

回过头看一下字面量生成的函数:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    const char *format = __cself->format; // bound by copy
    int value = __cself->value; // bound by copy
    printf(format, value);
}

这样就体现了为什么调用时要传入 block 本身,因为需要从中获取到捕获的变量。

Block 在内存中的位置

Block 实际上就是 C 结构体,所以它在内存中也只会是在全局区、栈或者堆上了。上面的例子中的 block 都是在堆上的,所以它们的 isa 是指向 _NSConcreteStackBlock 的。如果声明一个全局 block 变量,或者 block 没有捕获任何变量,则这个 block 的 isa 是指向 _NSConcreteGlobalBlock

那什么时候 Block 会到堆上去呢,毕竟在栈上的话栈的 frame 一结束就会自动销毁了。归根结底,需要调用 Block_copy() 这个宏,Block 才会被复制到堆上去,与之相对的有 Block_release() 这个宏,类似于引用计数的 retainrelease

观察使用 Block_copy() 或者查看它的源码(其中的 _Block_copy 函数)可以知道它的行为:

  • 对于一个 _NSConcreteGlobalBlock 来说,使用 Block_copy() 是没有作用的,会直接返回传入的 block;
  • 对于一个 _NSConcreteStackBlock 来说,会在堆上新建一份拷贝,并设置好引用计数,还会将 isa 指向 _NSConcreteMallocBlock
  • 对于一个 _NSConcreteMallocBlock 来说,使用 Block_copy() 则是将其引用计数增加 1。

当然,Block_release 也只对指向 _NSConcreteMallocBlock 的 block 有效果。

在 C 语言中使用 Block 时,就只能自己手动的调用 Block_copyBlock_release 了,但是在 Objective-C 中,Block 的拷贝工作基本上被 ARC 和系统库给管理了(比如 GCD 会对 block 进行拷贝)。

捕获 Block

上面提到 block 可以从栈上复制到堆上,对于捕获的如 intfloat 等基本的值类型变量来说,复制的时候只要直接赋值过去即可,但是如果捕获了一个 block 的话,情况就有所不同了,先看到下面的例子:

int main(void) {
    int value = 42;
    void (^block)(void) = ^{
        printf("value = %d", value);
    };
    void (^block1)(void) = ^{
        block();
    };
    block1();
    return 0;
}

变量 block 生成的代码与之前描述的一致,但是 block1 生成的代码就有一些不同了:

struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  struct __block_impl *block;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, void *_block, int flags=0) : block((struct __block_impl *)_block) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static struct __main_block_desc_1 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_1*, struct __main_block_impl_1*);
  void (*dispose)(struct __main_block_impl_1*);
} __main_block_desc_1_DATA = { 0, sizeof(struct __main_block_impl_1), __main_block_copy_1, __main_block_dispose_1};

除了 __main_block_impl_1 中捕获的 struct __block_impl *block 以外,在 __main_block_desc_1 多出了 copydispose 两个函数指针,这两个函数指针指向两个生成的帮助函数:

static void __main_block_copy_1(struct __main_block_impl_1*dst, struct __main_block_impl_1*src) {
    _Block_object_assign((void*)&dst->block, (void*)src->block, 7/*BLOCK_FIELD_IS_BLOCK*/);
}

static void __main_block_dispose_1(struct __main_block_impl_1*src) {
    _Block_object_dispose((void*)src->block, 7/*BLOCK_FIELD_IS_BLOCK*/);
}

其中 copy 是在 block1 被拷贝时调用,dispose 则是拷贝的 block1 在销毁时被调用。copy 函数中调用了 _Block_object_assign 函数,因为捕获的 block 需要进行内存管理,并传入了 BLOCK_FIELD_IS_BLOCK 标示是一个 block 赋值,在 _Block_object_assign 函数中可以看到,对于 block 类型的变量,会进行拷贝操作:

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

        *dest = _Block_copy(object);
        break;

这样便能保证捕获 block 不会被销毁,并保证内存管理的正确性。

同样的,在销毁时调用的 _Block_object_dispose 函数会对 block 类型的变量进行 release 操作:

      case BLOCK_FIELD_IS_BLOCK:
        _Block_release(object);
        break;

捕获对象

捕获 Objective-C 对象的时候,情况跟捕获 block 是几乎一样的(block 可以也被 Objective-C 当作为对象)。除了生成的对 _Block_object_assign_Block_object_dispose 函数的调用参数有所不同,对于如下的例子:

int main(void) {
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init];
        void (^block)(void) = ^{
            NSLog(@"object = %@", object);
        };
        block();
    }
    return 0;
}

生成的帮助函数如下:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

可以看到最后的参数都变成了 BLOCK_FIELD_IS_OBJECT,在 _Block_object_assign 的源码中:

      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
        *dest = object;
        break;

可以看到是调用了 _Block_retain_object,这是一个函数指针,会被 Objective-C 改写成 retain 方法,达到对 Objective-C 对象捕获(防止其释放)的效果。

__main_block_dispose_0 函数中也是同理了:

      case BLOCK_FIELD_IS_OBJECT:
        _Block_release_object(object);
        break;

⚠️ 上面帮助函数中对于 _Block_object_assign_Block_object_dispose 函数的调用在 ARC 下是不存在的。

实际在一个 ARC 开启的环境下给 _Block_object_assign 函数加断点是可以发现它是不会被调用的,猜想帮助函数实际上是这样的:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    objc_storeStrong(&dest->object, src->object);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    objc_storeStrong(&src->object, nil);
}

实际运行时断点图,__copy_helper_block_ 就是 __main_block_copy_0__destroy_helper_block_ 就是 __main_block_dispose_0

这样 __weak 变量才能使用,对于 __weak 变量的话则是调用 objc_copyWeak 函数。

__block

对于下面的例子:

int main(void) {
    __block int val = 0;
    void (^blk)(void) = ^{
        val = 42;
    };
    blk();
    return 0;
}

使用 clang -rewrite-objc 后,可以看到 main 函数中 val 变量的声明变的比较复杂了:

__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};

其实就是被转化成了一个叫做 __Block_byref_val_0 的结构体:

struct __Block_byref_val_0 {
  void *__isa;
  __Block_byref_val_0 *__forwarding;
  int __flags;
  int __size;
  int val;
};

使用的时候是这个样子的:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
  (val->__forwarding->val) = 42;
}

在捕获了 __block 变量的 block 被复制到堆上时,__block 变量也会被复制到堆上,这个时候 __forwarding 就会指向堆上的拷贝,保证数据的一致。

如果 __block 修饰的变量是 Objective-C 对象的话,会和 block 一样生成拷贝和销毁帮助函数:

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
  _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
  _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

struct __Block_byref_obj_0 {
  void *__isa;
  __Block_byref_obj_0 *__forwarding;
  int __flags;
  int __size;
  void (*__Block_byref_id_object_copy)(void*, void*);
  void (*__Block_byref_id_object_dispose)(void*);
  __strong id obj;
};

最终还是使用的 _Block_object_assign_Block_object_dispose。套路和 block 基本相似,就不细说了。

⚠️ 上面帮助函数中对于 _Block_object_assign_Block_object_dispose 函数的调用在 ARC 下也是不存在的。

可以看到 _Block_object_assign 处理 __block 变量的部分:

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

注意到注释:

// Note this is MRC unretained __block only. 
// ARC retained __block is handled by the copy helper directly.

这也解释了为什么 __block 修饰符在 MRC 下是不被捕获,在 ARC 下是被捕获的。

参考