Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这个新功能“Blocks”。从那开始,Block就出现在iOS和Mac系统各个API中,并被大家广泛使用。一句话来形容Blocks,带有自动变量(局部变量)的匿名函数。Block在OC中的实现如下:
从结构图中很容易看到isa,所以OC处理Block是按照对象来处理的。在iOS中,isa常见的就是_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock这3种(另外只在GC环境下还有3种使用的_NSConcreteFinalizingBlock,_NSConcreteAutoBlock,_NSConcreteWeakBlockVariable,本文暂不谈论这3种,有兴趣的看看官方文档)以上介绍是Block的简要实现,接下来我们来仔细研究一下Block的捕获外部变量的特性以及__block的实现原理。
研究工具:clang
为了研究编译器的实现原理,我们需要使用 clang 命令。clang 命令可以将 Objetive-C 的源码改写成 C / C++ 语言的,借此可以研究 block 中各个特性的源码实现方式。该命令是clang -rewrite-objc block.c
目录1.Block捕获外部变量实质
2.Block的copy和release.
Block捕获外部变量实质拿起我们的Block一起来捕捉外部变量吧。说到外部变量,我们要先说一下C语言中变量有哪几种。一般可以分为一下5种:自动变量函数参数静态变量静态全局变量全局变量研究Block的捕获外部变量就要除去函数参数这一项,下面一一根据这4种变量类型的捕获情况进行分析。我们先根据这4种类型自动变量静态变量静态全局变量全局变量写出Block测试代码。这里很快就出现了一个错误,提示说自动变量没有加__block,由于__block有点复杂;
我们先实验静态变量,静态全局变量,全局变量这3类。测试代码如下:
运行结果Block 外: global_i = 2,static_global_j = 3,static_k = 4,val = 5
Block 中 global_i = 3,static_global_j = 4,static_k = 5,val = 4
这里就有两点需要弄清楚了1.为什么在Block里面不加__bolck不允许更改变量?
2.为什么自动变量的值没有增加,而其他几个变量的值是增加的?自动变量是什么状态下被block捕获进去的?
为了弄清楚这两点,我们用clang转换一下源码出来分析分析。
首先全局变量global_i和静态全局变量static_global_j的值增加,以及它们被Block捕获进去,这一点很好理解,因为是全局的,作用域很广,所以Block捕获了它们进去之后,在Block里面进行++操作,Block结束之后,它们的值依旧可以得以保存下来。接下来仔细看看自动变量和静态变量的问题。在__main_block_impl_0中,可以看到静态变量static_k和自动变量val,被Block从外面捕获进来,成为__main_block_impl_0这个结构体的成员变量了。
接着看构造函数,__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val)这个构造函数中,自动变量和静态变量被捕获为成员变量追加到了构造函数中。main里面的myBlock闭包中的__main_block_impl_0结构体,初始化如下void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;impl.FuncPtr = __main_block_impl_0;
Desc = &__main_block_desc_0_DATA;
*_static_k = 4;val = 4;到此,__main_block_impl_0结构体就是这样把自动变量捕获进来的。也就是说,在执行Block语法的时候,Block语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是Block自身中。这里值得说明的一点是,如果Block外面还有很多自动变量,静态变量,等等,这些变量在Block里面并不会被使用到。那么这些变量并不会被Block捕获进来,也就是说并不会在构造函数里面传入它们的值。Block捕获外部变量仅仅只捕获Block闭包里面会用到的值,其他用不到的值,它并不会去捕获。再研究一下源码,我们注意到__main_block_func_0这个函数的实现static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int *static_k = __cself->static_k; // bound by copy int val = __cself->val; // bound by copy
global_i ++;
static_global_j ++;
(*static_k) ++;
NSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val); }我们可以发现,系统自动给我们加上的注释,bound by copy,自动变量val虽然被捕获进来了,但是是用 __cself->val来访问的。Block仅仅捕获了val的值,并没有捕获val的内存地址。所以在__main_block_func_0这个函数中即使我们重写这个自动变量val的值,依旧没法去改变Block外面自动变量val的值。
OC可能是基于这一点,在编译的层面就防止开发者可能犯的错误,因为自动变量没法在Block中改变外部变量的值,所以编译过程中就报编译错误。错误就是最开始的那张截图。Variable is not assignable(missing __block type specifier)小结一下:;到此为止,上面提出的第二个问题就解开答案了。自动变量是以值传递方式传递到Block的构造函数里面去的。Block只捕获Block中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。Block捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量。上面例子也都证明过了。
剩下问题一我们还没有解决。回到上面的例子上面来,4种变量里面只有静态变量,静态全局变量,全局变量这3种是可以在Block里面被改变值的。仔细观看源码,我们能看出这3个变量可以改变值的原因。静态全局变量,全局变量由于作用域的原因,于是可以直接在Block里面被改变。他们也都存储在全局区。静态变量传递给Block是内存地址值,所以能在Block里面直接改变值。根据官方文档我们可以了解到,苹果要求我们在自动变量前加入 __block关键字(__block storage-class-specifier存储域类说明符),就可以在Block里面改变外部自动变量的值了。
总结一下在Block中改变变量值有2种方式,一是传递内存地址指针到Block中,二是改变存储区方式(__block)。先来实验一下第一种方式,传递内存地址到Block中,改变变量的值。
控制台输出:
Block 外 str = Hello,
Block 中 str = Hello,World!
在__main_block_func_0里面可以看到传递的是指针。所以成功改变了变量的值。