从源码看 Block

2,381
原文链接: www.jianshu.com

这篇文章是自己看源代码以及一些文章总结出来的,如有错误希望有大神指正。

需要先知道这些

Block 在底层是以结构体的形式实现的,Block 的函数体会被分离出来成为一个单独的 C 函数,最后在调用 Block 的时候是以函数指针的形式进行调用。

同时 Block 可能会捕获一些值,保存到结构体中。Block 能够捕获自动变量(不能修改)、静态局部变量(可以修改)。全局变量因为作用域的原因不会被 Block 捕获,可以直接使用。

Block 有3种类型:_NSConcreteStackBlock_NSConcreteMallocBlock 以及 _NSConcreteGlobalBlock

比如说有一段代码:

int val = 10;
void (^block)() = ^{ NSLog(@"%d", val); };
block();

C++ 重写,简化之后:

struct __main_block_impl_0 {
  void *isa;
  void *FuncPtr;
  int val;
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy
  NSLog(@"%d", val);
}
int main(int argc, const char * argv[]) {
  int val = 10;
  struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, val);
  struct __main_block_impl_0 *block = &tmp;
  (*block->impl.FuncPtr)(block);
  return 0;
}

Block 的实现

Block_private.h 中有关于 Block 的实现:

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

可以与 C++ 重写后的结构体比较一下,其实是一样的。用 Block_layout 来表示上一个例子,其实就是会编译成这样(简化):

struct __block_literal_1 {
  void *isa; // = _NSConcreteStackBlock
  void (*invoke)(struct __block_literal_1 *); // = __main_block_func_0
  struct __block_descriptor_1 *descriptor;
  const int val; // 10
}
static struct __block_descriptor_1 {
  unsigned long int Block_size; // = sizeof(struct __block_literal_1)
}

当一个 Block 被初始化的时候,有以下几点:

  1. invoke 函数指针会指向一个 C 函数,比如上文中的 __main_block_func_0,这个函数以 Block 结构体作为第一个参数,剩下的参数就是调用 Block 的时候传递的参数(如果有的话)。
  2. isa 会被赋值为 _NSConcreteStackBlock。如果 Block 定义在全局的区域或者 Block 仅仅捕获了全局变量(或静态局部变量),则 isa 被赋值为 _NSConcreteGlobalBlock
  3. 如果存在相关的 copy_helper 方法和 dispose_helper 方法,则会与 copydispose 这两个函数指针联系起来(后文 Copy & Dispose 章节)。

另外我找到这样一句话是关于 _NSConcreteStackBlock_NSConcreteGlobalBlock

that is, a different address is provided as the first value and a particular (1<<28) bit is set in the flags field, and otherwise it is the same as for stack based Block literals. This is an optimization that can be used for any Block literal that imports no const or __block storage variables.

我有点想不通为什么捕获全局变量(或静态局部变量)的 Block 就是 _NSConcreteGlobalBlock,看了上面这段话之后还是很模糊(技术不行英文又渣),如果有大神知道的话期待回复。

特别要注意的是,如果 Block 没有捕获变量,或者仅仅捕获了全局变量(或静态局部变量),用 clang 重写之后,它的 isa 指针的值依然是 _NSConcreteStackBlock,只有定义在全局区域的 Blockclang 重写之后是 _NSConcreteGlobalBlock。但是用 Xcode 运行的话,以上情况都是 _NSConcreteGlobalBlock,我想这可能是因为编译器的处理,这方面也不是很清楚。

__block 的实现

Block_private.h 中有关于 __block 的实现:

struct Block_byref {
  void *isa;
  struct Block_byref *forwarding;
  volatile int32_t flags; // contains ref count
  uint32_t size;
};

struct Block_byref_2 {
  // requires BLOCK_BYREF_HAS_COPY_DISPOSE
  void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
  void (*byref_destroy)(struct Block_byref *);
};

C++ 重写之后的结构体也是一样的:

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

当一个 __block 结构体被初始化的时候,原始值会成为该结构体的一个成员变量,同时其中的 __forwarding 指针指向结构体自己。

Block 初始化相同的是,如果必要的话也会存在 copy_helperdispose_helper 方法分别与 byref_keepbyref_destroy 这两个函数指针联系起来。

Copy & Dispose

ARC 的环境下,_NSConcreteStackBlock 类型的 Block 很多情况下都会从栈拷贝到堆,变成 _NSConcreteMallocBlock

假如有以下代码:

typedef void (^block)(void);
block func() { 
  int val = 3;
  block blk = ^{ NSLog(@"%d", val); };
  return blk; 
}

转换之后会有以下两个方法(我是这样转换的:clang -fobjc-arc -S filename):

  1. _objc_retainBlock
  2. _objc_autoreleaseReturnValue

在第一个方法中调用的就是 Block_copy 方法。

Copy

对象

如果 Block 引用了一个对象,可以看看 C++ 重写之后的代码:

NSObject *obj = [NSObject new];
void (^block)() = ^{ 
  NSLog(@"%@", obj); 
};
block();
struct __main_block_impl_0 {
  NSObject *__strong obj;
};

static struct __main_block_desc_0 {
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
}

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

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

不仅 block 强引用了 obj,还多出了两个函数指针:copydispose

__main_block_copy_0__main_block_dispose_0 这两个方法用来管理 obj 的生命周期。这两个函数中有 _Block_object_assign_Block_object_dispose 两个函数。在 runtime.c 中有它们的实现。

现在梳理一下这个例子中 Block 从栈拷贝到堆的调用栈:

  1. 调用 Block_copy,将栈上的 Block 拷贝到堆,isa 赋值为 _NSConcreteMallocBlock
  2. 调用 _Block_call_copy_helper
  3. 调用函数指针 copy 指向的函数,在这里是 __main_block_copy_0
  4. 调用 _Block_object_assignBLOCK_FIELD_IS_OBJECT 作为参数 flags 表示引用的是一个对象
  5. 调用 _Block_retain_object,但是这个函数没有任何实现。我觉得可以这样理解:在这个例子中,obj 本身就存在于堆内存中,Block 仅仅是通过值传递的形式拿到了指针,所以没必要从栈拷贝到堆。

__block 变量(非对象)

如果 Block 捕获了用 __block 修饰的变量,用 C++ 重写之后出现类似的情况:

__block int i = 10;
void (^block)() = ^{ 
  NSLog(@"%d", i); 
};
struct __main_block_impl_0 {
  __Block_byref_i_0 *i; // by ref
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
}

__block 变量成为了 Block 结构体的成员,flags 的值从 BLOCK_FIELD_IS_OBJECT

变为了 BLOCK_FIELD_IS_BYREF,因为现在的结构体使用的是 __block 变量。

这个例子中 Block 从栈拷贝到堆的过程与上个例子不同的是在 _Block_object_assign 中的调用,调用了 _Block_byref_copy 函数。在该函数中:

struct Block_byref *copy = (struct Block_byref *)malloc(src->size);

这条语句从堆内存开辟了一块空间,将 __block 变量的结构体从栈拷贝到堆。

copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy;  // patch stack to point to heap copy

这两条语句保证了无论是在 Block 内访问 __block 变量还是在 Block 外访问,我们访问到的都是同一个变量。

__block 变量(对象)

如果 Block 引用的是一个 __block 修饰的对象:

__block NSObject *obj = [NSObject new];
void (^block)() = ^{ 
  NSLog(@"%@", obj); 
};
struct __Block_byref_obj_0 {
  void (*__Block_byref_id_object_copy)(void*, void*);
  void (*__Block_byref_id_object_dispose)(void*);
  NSObject *__strong obj;
};

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);
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}

在这个例子的调用栈中,区别在于 _Block_byref_copy,会进入到这个分支:

if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
  (*src2->byref_keep)(copy, src);
}

最后调用的是 __Block_byref_id_object_copy 指向的函数,也就是 __Block_byref_id_object_copy_131_Block_object_assign 函数中的 131 参数其实是 BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT,前半部分的意思是此次调用来自 __block 结构体的 byref_keep 函数指针。

除了上文提到的 BLOCK_FIELD_IS_BYREFBLOCK_FIELD_IS_OBJECT,还有另外一个值是 BLOCK_FIELD_IS_BLOCK。这个值表示在 Block 中引用了其他的 Block

Dispose

释放一个 Block 的调用栈其实与 copy 类似。

  1. 首先会调用 _Block_release 函数,如果是 _NSConcreteGlobalBlock 类型的 Block 则不需要释放
  2. 调用 _Block_call_dispose_helper
  3. 访问 Block 结构体中的 dispose 成员变量,调用其指向的函数,比如是 __main_block_dispose_0
  4. 最后调用 _Block_object_dispose

简单说说循环引用

首先是第一种情况:

__weak __typeof(self) weakSelf = self;
self.block = ^{
  NSLog(@"%@", weakSelf.property);
};

我们可以看到 block 确实是对 self 只有弱引用。

struct __Rectangle__addBlock_block_impl_0 {
  struct __block_impl impl;
  struct __Rectangle__addBlock_block_desc_0* Desc;
  Rectangle *__weak weakSelf;
};

第二种情况:

__weak __typeof(self) weakSelf = self;
self.block = ^{
  __strong __typeof(weakSelf) strongSelf = weakSelf;
  NSLog(@"%@", weakSelf.property);
};

在这里,block 在内部新建了一个 strongSelfself 进行了一次强引用,但是 block 本身并没有强引用。


参考

Block Implementation Specification