关于 Block 的一些知识点

Block 实际结构

先看一段示例

1
2
3
4
5
6
int age = 10;
void (^block)(int) = ^(int a){
NSLog(@"%d",age);
};

block(20);

将该段代码,反编译为 C++ 看看内部实现

1
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

将生成的代码关键部分简化一下方便阅读

1
2
3
4
5
int age = 10;
void (*block)(int) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);

block->FuncPtr(block, 20);

实际生成一个__main_block_impl_0的结构体
名称__x_block_impl_y,x 为文件名,y 为 block 的编号(0、1、2…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age; // 所捕获的变量,都会放在这,这里属于值传递
// 结构体构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock; // 栈类型
impl.Flags = flags;
impl.FuncPtr = fp; // 函数具体的指针
Desc = desc;
}
};

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr; // 实际函数的指针,通过它调用 block
};

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; // block 的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

// 实际上就是 block 里的代码块
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a) {
int age = __cself->age; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_4x_pyqkl8qx7vzgklmhywl09sd40000gn_T_main_d14565_mi_0,age);

}

关于 Block 的变量捕获

block 实际属于跨函数调用,为了保证 block 内部能够正常访问外部的变量,block 有个变量捕获机制。

变量类型 是否捕获到 block内部 访问方式
局部变量 auto 值传递
static 指针传递
全局变量 × 直接访问

Block 的类型

block 有三种类型,通过调用 class 方法或 isa 指针查看具体类型,最终都是继承自 NSObject

  • __NSGlobalBlock__ (_NSConcreteGlobalBlock)
  • __NSStackBlock__ (_NSConcreteStackBlock)
  • __NSMallocBlock__ (_NSConcreteMallocBlock)

应用程序的内存分配

block 类型 环境
 __NSGlobalBlock__ 没有访问 auto 变量
__NSStackBlock__ 访问了 auto 变量
__NSMallocBlock__ __NSStackBlock__调用了 copy

Block copy

每一种类型 block 调用了 copy 后的结果

block 的类型 copy 后的存储区域 copy 效果
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteStackBlock 从栈区复制到堆
_NSConcreteMallocBlock 引用计数增加

在 ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上,如下

  • block 作为函数返回值时
  • 将 block 赋值给 __strong指针时
  • block 作为 Cocoa api 中方法名含有 usingBlock 的方法参数时
  • block 作为 GCD api 中的方法参数时

所以 MRC 下 block 作为属性时建议使用 copy 修饰符修饰;ARC 下可以用 strong,也可以延续 MRC 下的写法用 copy,都不影响,因为编译器会根据情况自动操作

对象类型的 auto 变量

  • 栈空间内的 block,不会强引用外部的 auto 变量。

  • 如果 block 被 copy 到堆上

    1. 会调用 block 内部的 copy 函数
    2. copy 函数内部会调用 _Block_object_assign函数
    3. _Block_object_assign函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unreatained)做出相应操作,形成层强引用(retain)或弱引用
  • 如果 block 从堆上移除

    1. 会调用 block 内部的 dispose 函数
    2. dispose 函数内部会调用 _Block_object_dispose 函数
    3. _Block_object_dispose 函数会自动释放引用的 auto 变量(release)
函数 调用时机
copy 函数 栈上的 block 复制到堆时
dispose 堆上的 block 被废弃时

看一下,block 内引用对象时,block 内部结构变化

1
2
3
4
5
6
7
Person *obj = [[Person alloc] init];
obj.age = 10;

void (^block)(void) = ^(){
NSLog(@"%d",obj.age);
};
block();

编译为 C++ 代码后,相比引用基本数据类型会有如下变化。

1
2
3
4
5
6
7
8
9
10
11
12
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*/);}

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
// 此结构体会多出如下两个,对内部访问的对象进行内存管理
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

__block 修饰符

__block用于解决 block 内部无法修改 auto 变量的问题,不能用于修改全局变量、静态变量(static)。

1
2
3
4
5
__block int age = 10;
void (^block)(void) = ^(){
age = 20;
NSLog(@"%d",age);
};

编译为 C++ 代码后,会有以下片段,__block变量会被包成一个 __Block_byref_age_0对象,所以在__main_block_desc_0也会存在 copydispose用于内存管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding; // 指向自己
int __flags;
int __size;
int age;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // 被 __block 修饰的变量会有一个结构体指针
};

__main_block_func_0函数内的实现

1
2
3
4
5
6
7
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 获取存储 __block 变量结构体的指针
__Block_byref_age_0 *age = __cself->age; // bound by ref
// 通过 __Block_byref_age_0 指针获取内部的 __forwarding 指针,再获取内部的变量
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4x_pyqkl8qx7vzgklmhywl09sd40000gn_T_main_3fdaf9_mi_0,(age->__forwarding->age));
}

__block 内存管理

  • 当 block 位于栈上时,不会对 __block 变量产生强引用
  • 当 block copy 到堆上时
    1. 会调用 block 内部的 copy 函数
    2. copy 函数调用 _Block_object_assign 函数
    3. _Block_object_assign会对 __block 变量形成强引用(retain 一次)

block 复制到堆上时,会将内部引用的__block变量一起拷贝到堆上。
因为__Block_byref_age_0是一个对象,而__main_block_impl_0引用了这个对象,所以需要该对象的内存管理操作。

block 复制到堆

  • 当 block 从堆上移除时
    1. 会调用 block 内部的 dispose 函数
    2. dispose 函数会调用 _Block_object_dispose函数
    3. _Block_object_dispose函数会自动释放引用的 __block变量(release 一次)

block 从堆上移除

ARC 下会对强引用的 block,自动复制到堆上,block 对__block变量会自动强引用;对普通的 auto 对象,会根据是__strong或是__weak形成强引用或者弱引用。
MRC 下将 block copy 到堆上,block 不会对__block 变量强引用。

对象类型的 auto变量、__block 变量

  • block 位于栈上时,对他们都不会产生强引用
  • block 位于堆上时,都会调用 copy 函数来处理他们
    1. __block变量

      1
      _Block_object_assign((void*)&dst->a, (void*)src->a, *BLOCK_FIELD_IS_BYREF*/);
    2. 对象类型的 auto 变量

      1
      _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
  • block 从堆上移除时,都会调用 dispose 函数来处理他们
    1. __block变量

      1
      2
      _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

    2. 对象类型的 auto 变量

      1
      2
      _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

循环引用问题

ARC 下可以使用 __weak __unsafe_unretained 解决,也可以使用 __block 解决,block 内将引用的对象置为 nil,但是必须调用 block。
MRC 下可以使用 __unsafe_unreatined__block解决