【OC源码】Block 源码实现分析

弄清楚 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 部分)
  • 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: 用于修饰这个变量的类型, 主要作用是在 copydispose 时遭到对应的策略(也是在源码解析中说明)
  • 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 的源码解析
执行过程很简单:

  1. 对要复制的入参判空,
  2. 判断 flags 是否是堆中的 block, 是则引用计数 +1 并结束
  3. 判断 flags 是否是 global 的 block, 是则啥都不干直接结束
  4. 其他情况下, 在堆中申请一块内存, 然后逐个字段赋值, 改 flags,引用计数赋值

释放时行为也是根据 flags, globalstrack 的由系统处理. 堆中的则调用在编译时构造函数中传入的 销毁函数, 销毁他的所有子字段. 最后执行 free 销毁自己

if (latching_decr_int_should_deallocate(&aBlock->flags)) {  
    _Block_call_dispose_helper(aBlock);  
    _Block_destructInstance(aBlock);  
    free(aBlock);  
}