本文会通过 clang 的 -rewrite-objc
选项来分析 block 的 C 转换源代码。其分析方式在该系列上一篇有详细介绍。请先阅读 浅谈 block(1) - clang 改写后的 block 结构 。
首先需要做代码准备工作,我们编写一段 block 引用外部变量的 c 代码。
编译运行成功后,使用 -rewrite-objc
进行改写。
clang -rewrite-objc block.c
简化代码后,得到以下主要代码:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; char *str; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int flags=0) : str(_str) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { char *str = __cself->str; // bound by copy printf("%s/n", str); } 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) }; int main() { char *str = "Desgard_Duan"; void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); return 0; }
与上一篇转换的源码不同的是,block 语法表达中的变量作为成员添加到了 __main_block_func_0
结构体中。
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; char *str; // 外部引用变量 }
并且,在该结构体中的应用变量类型与外部的类型完全相同。在初始化该结构体实例的构造函数也自然会有所差异:
void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str));
去掉强转语法简化代码:
void (*block)() = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, str);
在构造时,除了要传递自身(self) __main_block_func_0
结构体,而且还要传递 block 的基本信息,即 reserved 和 size 。这里传递了一个全局结构体对象 __main_block_desc_0_DATA
,因为他是为 block 量身设计的。最后在将引用值参数传入构造函数中,以便于构造带外部引用参数的 block。
进入构造函数后,发现了含有冒号表达的构造语法:
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int flags=0) : str(_str) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }
其实,冒号表达式是 C++ 中的一个固有语法。这是显示构造的方法之一。另外还有一种构造显示构造方式,其语法较为繁琐,即使用 this 指针构造。(关于 C++ 构造函数,可以学习 msdn 文档 构造函数 (C++) )
之后的代码与前一篇分析相同,不再讨论。
通过整个构造 block 流程分析,我们发现当 block 引用外部对象时,会在结构体内部新建立一个成员进行存储。此处我们使用的是 char* 类型,而在结构体中所使用的 char* 是结构体的成员,所以可以得知: block 引用外部对象时候,不是简单的指针引用(浅复制),而是一种重建(深复制)方式 (括号内外分别对于基本数据类型和对象分别描述) ) 。所以如果在 block 中对外部对象进行修改,无论是值修改还是指针修改,自然是没有任何效果。
上文中的 block 所引用的外部成员是一个字符型指针,当我们在 block 内部对其修改后,很容易的想到,会改变该指针的指向。而当 block 中引用外部变量为常用数据类型会有些许的不同:
我们来看这个例子 (这是来自 Pro multithreading and memory management for iOS and OS X 2.3.3 一节的例子):
int val = 0; void (^blk)(void) = ^{val = 1};
执行代码后会报 error :
error: variable is not assignable (missing __block type specifier) void (^blk)(void) = ^{val = 1};
上述书中对此情况是这样解释的:
block 中所使用的被截获自动变量如同“带有自动变量值的匿名函数”,仅截获自动变量的值。 block 中使用自动变量后,在 block 的结构体实力中重写该自动变量也不会改变原先截获的自动变量。
这应该是 clang 对 block 的引用外界局部值做的保护措施,也是为了维护 C 语言中的作用域特性。既然谈到了作用域,那么是否可以使用显示声明存储域类型从而在 block 中修改该变量呢?答案是可以的。当 block 中截取的变量为静态变量(static),使用下例进行试验:
int main() { static int static_val = 2; void (^blk)(void) = ^{ static_val = 3; }; }
装换后的代码:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int *static_val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; int main() { static int static_val = 2; void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_ return 0; }
会发现在构造函数中使用的静态指针 int *_static_val
对其进行访问。将静态变量 static_val
的指针传递给 __main_block_impl_0
结构体的构造函数并加以保存。通过指针进行作用域拓展,是 C 中很常见的思想及做法,也是超出作用域使用变量的最简单方法。
那么我们为什么在引用自动变量的时候,不使用该自动变量的指针呢?是应为在 block 截获变量后,原来的自动变量已经废弃,因此block 中超过变量作用域从而无法通过指针访问原来的自动变量。
为了解决这个问题,其实在 block 扩展中已经提供了方法( 官方文档 )。即使用 __block
关键字。
__block
关键字更准确的表达应为 __block说明符(__block storage-class-specifier) ,用来描述存储域。在 C 语言中已经存有如下存储域声明关键字:
__block
关键字类似于 static
、 auto
、 register
,用于将变量存于指定存储域。来分析一下在变量声明前增加 __block
关键字后 clang 对于 block 的转换动作。
__block int val = 1; void (^blk)(void) = ^ { val = 2; };
// 要点 1:__block 变量转换结构 struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_val_0 *val; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_val_0 *val = __cself->val; // bound by ref // 要点 2:__forwarding 自环指针存在意义 (val->__forwarding->val) = 2; } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; // 要点 3:copy/dispose 方法内部实现 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 }; int main() { __attribute__((__blocks__(byref))) __Block_byref_val_0 val = { (void*)0, (__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1 }; void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344)); return 0; }
发现核心代码部分有所增加,我们先从入口函数看起。
__Block_byref_val_0 val = { (void*)0, (__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1 };
原先的 val 变成了 __Block_byre_val_0
结构体类型变量。并且这个结构体的定义是之前未曾见过的。并且我们将 val 初始化的数值 1,也出现在这个构造中,说明该结构体持有原成员变量。
struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; };
在 __block
变量的结构体中,除了有指向类对象的 isa
指针,对象负载信息 flags
,大小 size
,以及持有的原变量 val
,还有一个自身类型的 __forwarding
指针。从构造函数中,会发现一个有趣的现象, __forwarding
指针会指向自身,形成自环 。后面会详细介绍它。
而在 block 体执行段,是这样定义的。
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) = 2; }
第一步中获得 val 的方法和 block 中引用外部变量的方式是一致的,通过 self 来获取变量。而对于外部 __block 变量赋值的时候,这种写法引起了我们的注意: (val->__forwarding->val) = 2;
,这样做的目的何在,在后文会做出分析。
当 block 内部引用外部的 __block 变量,会使用以上结构对 __block 做出转换。另外,该结构体并不声明在 __main_block_impl_0
block 结构体中,是因为这样可以对多个 block 引用 __block 情况下,达到复用效果,从而节省不必要的空间开销。
__block int val = 0; void (^blk1)(void) = ^{val = 1;}; void (^blk2)(void) = ^{val = 2;};
只观察入口方法:
__Block_byref_val_0 = {0, &val, 0, sizeof(__Block_byref_val_0), 10}; blk1 = &__main_block_impl_0(__main_block_func_0 , &__main_block_desc_0_DATA , &val , 0x22000000); blk2 = &__main_block_impl_0(__main_block_func_1 , &__main_block_desc_1_DATA , &val , 0x22000000);
发现 val 指针被复用,使得两个 block 同时使用一个 __block 只需要对其结构声明一次即可。
通过两篇文的 block 的结构转换,我们发现其实 block 的实质是一个 对象 (Object) ,从封装成结构体对象,再到 isa 指针结构,都是明显的体现。对于 __block 也是如此,在转换后将其封装成了 __block 结构体类型,以对象方式处理。
带着 C 代码中的 block 扩展转换规则开始进入 Objective-C block 的学习。首先需要知道 block 的三个类型。
类型 | 对象存储域 | 地址单元 |
---|---|---|
_NSConcreteStackBlock | 栈 | 高地址 |
_NSConcreteMallocBlock | 堆 | |
_NSConcreteGloalBlock | 静态区(.data) | 低地址 |
在上一篇文中的末尾部分,简单的说了一下全局静态的存储问题。这里再一次强调, _NSConcreteGloalBlock
的 block 会在一下两种情况下出现(与 clang 转换结果不大相同):
而在其他情况下,基本上 block 的类型都为 _NSConcreteStackBlock 。但是在栈上的 block 会受到作用域的限制,一旦所属的变量作用域结束,该 block 就会被释放。由此,引出了 _NSConcreteMallocBlock 堆 block 类型。
block 提供了将 block 和 __block 变量从栈上复制到堆上的方法来解决这个问题。将配置在站上的 block 复制到堆上,这样可以保证在 block 变量作用域结束后,堆上仍旧可访问。
__block 变量通过 __forwarding 可以无论在堆上还是栈上都能正常访问。当 block 存储在堆上的时候,对应的栈上 block 的 __forwarding 成员会断开自环,而指向堆上的 block 对象。这也就是 __forwarding 指针存在的真实用意。
在复制到堆的过程中,__forwarding 指针是如何更改指向的?这个问题在下一篇中进行介绍。这篇文主要讲述了 __block 变量在 block 中的结构,以及如何获取外部变量,并可以对其修改的详细过程,希望有所收获。