弄清楚 block 原理借住 clang 编译代码,及 block 的源码.
这两部分我总结在:
这篇文章直接上一些得出的结论, 具体需要了解代码部分会跳会另外两篇文章中
先上个结论:
Block 是一个结构体, 其内部带有函数指针,指向其定义时的函数体. 函数体中引用的外部变量, 会被赋值给 block 对应的字段,并在函数体执行时,将 block 作为参数传入,然后复制其中的变量.
Block 的本质
这部分内容看 其他: clang 编译后的的 block 解析 比较清楚, 其被编译后最终长这样

首先是 OC 定义时的 block 函数体, 跟 OC 中其他函数类似, 在编译后会变成 C 中全局函数
这个函数不是和 block 定义的函数体完全一致的:
- 首先函数的入参全部会变成 block 结构体类型(定义时的 block 是哪个就会生成对应的 block 类型)
- 如果有使用外部变量, 函数体最前面会多出局部变量赋值的语句
^{
printf("I am block")
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 如果引用了外部变量, 这里会生成对应名字的局部变量, 取值只 cself 中(copy 的)
MyObject *myObj = __cself->myObj; // bound by copy
// 如果没有引用外部变量, 就是直接函数内部的内容
printf("I am block")
}
然后 block 本身会变成长得跟源码中Block_layout 基本一样的结构体, 其应用的外界 局部 变量会被编译器生成为结构体中同名字段
flags: 用于标记当前 block 的类型- 没有外部变量的 block 被称为
_NSConcreteGlobalBlock(isa 会指向他), 在使用时直接使用, 不用复制 - 有外部变量的 block 在定义时是
_NSConcreteStackBlock, 顾名思义, 是存放在栈区中的, 栈区中的 block 使用时会被赋值到堆区 - 已经复制过到堆区的, 引用时则是直接引用计数+1
(上述流程描述在其他: block 的源码解析, _Block_copy 部分)
- 没有外部变量的 block 被称为
invoke: 会在初始化时直接持有对应的全局函数指针,就上面那个descriptor: 中存放一些描述信息, 比如有外部变量的 block, 会在这个结构体中存着处理外部变量的copy,dispose函数- 最后就是根据外部变量生成字段表(需要主要的是全局的外部变量, 不会被 block 引用, 而是直接调用)
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
Block 的类型
上面提到 flags 中记录了当前 block 的类型, 其源码中定义了这么多种
void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };
我们使用时,多接触的是 _NSConcreteGlobalBlock, _NSConcreteStackBlock, _NSConcreteMallocBlock
不同类型的 block 他们的 isa 指向会不一样. 由 flags 描述其类型
_NSConcreteGlobalBlock:- 没有用到外部变量的 block 是这个类型, 可以理解为就是全局的静态变量
- 在 block 执行
_Block_copy时不会有实质操作
_NSConcreteStackBlock:- 在 block 是某个指针指向前, 都会在栈区
- 因为在栈区的东西, 一离开函数就被销毁了
- 所以在指针指向这个 block 后, 调用
_Block_copy执行 copy 操作
_NSConcreteMallocBlock- 存放在堆区, 有外部变量引用的使用时都会是这个类型
- 最终由 autoreleasepool 销毁
(_Block_copy写在这 其他: block 的源码解析)
Block 对外界局部变量的捕获
首先, 为什么是局部变量? 因为全局变量可以被 block 任意使用,不会有销毁问题, 所以不被描述在 block 中.
而局部变量, 完全有可能在 block 执行时已经被销毁, 需要 block 对局部变量先持有,防止被修改.
我写了个小 demo, 编译后分析 其他: clang 编译后的的 block 解析
可以看到的是, 引用类型 和 基本数据类型(在不修改的情况下) 会直接在 block 结构体中直接生成对应字段
下面代码中 myObj, myObj2, mainInt(在不声明 __block 时就是 int 型) 就是
而如果希望在 block 中修改基本数据类型, 会被编译器报错,需要加__block
// OC 中
void (^blockWithVar)(void) = ^{
myObj;
myObj2;
mainInt;
}
// 编译后
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
MyObject *myObj;
MyObject *myObj2;
__Block_byref_mainInt_0 *mainInt; // by ref
//...省略构造函数
};
除了生成对应字段外, 还会同时生成两个函数, 用于帮助 block 在 copy 和 release 时, 对其拥有的字段做管理
内部是对所有从外部来的字段做 _Block_object_assign 与 _Block_object_dispose
其他: block 的源码解析
static void __main_block_copy_1(struct __main_block_impl_1*dst, struct __main_block_impl_1*src) {
//内部是逐个外部变量调用 `_Block_object_assign`
_Block_object_assign((void*)&dst->mainInt, (void*)src->mainInt, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_1(struct __main_block_impl_1*src) {
//内部是逐个外部变量调用 `_Block_object_dispose `
_Block_object_dispose((void*)src->myObj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
引申一个问题? 为什么 block 内部不允许修改基本数据类型?
- 首先和 block 被设计的引用场景有关, block 在使用时,大部分情况都不是当场执行的. 也就是他真正运行时,外部环境已经没了
- 基于上面这点, 同时我们既然要在 block 中使用外部变量,肯定是对当时的上下文有使用需求, 所以 block 对外部的内容做一次 copy, 留下了当时环境下的内容. (不直接引用,我理解为原先的上下文不由 block 创建, 不能由 block 影响 当时环境 的变量释放)
- 此时 block 已经无法修改外界的值了, 他的修改只会对其内部的 copy 生效 (这里注意区分引用类型和基本类型)
所以编译器就被设置为判断这种修改是错误行为 - 为了在某些情况下, 也能达到 block 内部修改, 影响到外部的效果,就增加了
__block字段
(这个字段不是只对基本数据类型生效, 而是因为基本数据类型才有这么用的必要.) (指针和他的 copy 指向同一个地址, 都是有能力修改源值的)
被__block修饰的变量, 会在编译后变成一个结构体, 这个结构体与源码中 struct Block_byref 类似
其他: block 的源码解析
flags: 用于修饰这个变量的类型, 主要作用是在copy和dispose时遭到对应的策略(也是在源码解析中说明)forwarding: 若是在栈中, 会指向自己, 若是在堆中会指向堆中的 byref
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
细聊一下 forwarding, 想象一个场景, 有一个变量在 block 外部, block 内部也需要修改这个变量此时代码如下
__block int count = 0
void (^block)(void) = ^{ count++; }
count++;
前面提到过, block 在赋值时会被 copy, 从栈区 copy 一份到堆区.
这时 count 也会被复制到堆区.
那么加上原本的 count, 此时有个同名的变量,同时在堆区,也在栈区
forwarding 就是为了避免这个问题设计的
__block 标识变量会被转成成 struct Block_byref count, 此时他在栈区
然后在 block 被 copy 后, 堆区也有一个变量. 这时栈区的 forwarding 就会指向堆区的 struct Block_byref count
然后不同堆区或栈区的值修改, 就都是对同一个值做修改了.
Block 的行为
前面提到过刚定义时的 block 是存放在栈中(有外部变量的情况)的, 但是在使用时却会 copy 一份, 跑到堆中
这个 copy 行为由_Block_copy执行, 函数源码在其他: block 的源码解析
执行过程很简单:
- 对要复制的入参判空,
- 判断 flags 是否是堆中的 block, 是则引用计数 +1 并结束
- 判断 flags 是否是 global 的 block, 是则啥都不干直接结束
- 其他情况下, 在堆中申请一块内存, 然后逐个字段赋值, 改 flags,引用计数赋值
释放时行为也是根据 flags, global 与 strack 的由系统处理. 堆中的则调用在编译时构造函数中传入的 销毁函数, 销毁他的所有子字段. 最后执行 free 销毁自己
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
free(aBlock);
}