摘要
这篇文章是我自己对 block 的使用和理解的总结吧,算是笔记。
什么是 Block?
介绍 block 之前,先讲一下什么是闭包吧。在wikipedia 上,闭包 的定义是:
In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
翻译一下就是:
闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。
而 block 实际上就是 Objective-C 语言对于闭包的实现。
我们要明确两点:
- Block可以访问Block函数以及语法作用域以内的外部变量。也就是说:一个函数里定义了个block,这个block可以访问该函数的内部变量(当然还包括静态,全局变量)-即block可以使用和本身定义范围相同的变量。
- Block其实是特殊的Objective-C对象,可以使用copy,release等来管理内存,但和一般的NSObject的管理方式有些不同,下面会稍加说明。
Block 的语法
Block很像函数指针,这从Block的语法上就可以看出。
Block的原型:
返回值 (^名称)(参数列表)
Block的定义
^ 返回值类型 (参数列表) { 表达式 }
其中返回值类型和参数列表都可以省略,最简单的Block就是:
1 | ^{ ; }; |
一般的定义就是:
返回值 (^名称)(参数列表) = ^(参数列表){代码段};
为了方便通常使用typedef定义:
1 | typedef void (^block) (void); |
Block 根据储存的区域分为三种类型
每一种类型的 block,我都会举一下个例子。为了跟好的理解 block,下面的代码都是不使用 ARC 的(即,使用 MRC)。
_NSConcreteGlobalBlock-存储在全局数据区域(和全局变量一样)
1 |
|
输出结果:
解释:
当一个 block 没有访问外部变量或者只访问了全局变量(或block内变量)时,block 的类型是 _NSConcreteGlobalBlock。虽然 block1 block2 block3 都是 储存在全局的数据区,但是他们在结构上还是有些不一样的。
clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,我们利用 clang 查看 block 具体的源码实现方式。在命令行中输入:
1 | clang -rewrite-objc <文件名.m> |
可以在相同的目录下面看到对应的 cpp 文件。
main函数:
main函数中对block的创建和调用,可以看出执行block就是调用一个以block自身作为参数的函数,这个函数对应着block的执行体。
block1:
block1由结构体 __main_block_impl_0 __main_block_func_0函数 和 __main_block_desc_0结构体 组成。
结构体 __main_block_impl_0,可以看出是根据所在函数(main函数)以及出现序列(第0个)进行命名的。 __main_block_impl_0中包含了两个成员变量和一个构造函数,成员变量分别是 __block_impl结构体和描述信息Desc,之后在构造函数中初始化block的类型信息和函数指针等信息。
__main_block_func_0函数,即block对应的函数体。该函数接受一个__cself参数,即对应的block自身。
再下面是__main_block_desc_0结构体,其中比较有价值的信息是block大小。
block2:
而 block2 相比于我后面写的 block4 ,差别主要在于main函数里传递的是a的地址(&a),以及 __main_block_impl_1结构体中成员a变成指针类型(int*),实际是进行了指针的拷贝,将结构体传入 __main_block_func_1 函数中也是用一个临时变量 (int*)来储存 a 的内存地址。
然后在执行block时,通过指针修改值。
block3:
block3 中有 int 变量 a,因为a 存在于 block 的内部,所以不用像 block 外部参数是一样被拷贝到结构体 __main_block_impl_2 之中,只是将 block 内的临时变量 a 存在 __main_block_func_0 之中。
_NSConcreteStackBlock-存储在栈上
1 |
|
输出结果:
解释:
简单一句话来说就是:当一个 block 访问了外部变量时,那它就是_NSConcreteStackBlock 类型的block。
block4源码实现:
可以结合 block2 和 block3 进行理解,block4 将外部变量拷贝进 结构体__main_block_impl_0,而在 __main_block_func_0 中用临时变量 a 来存储由结构体传过来外部变量 a。
那如果不只是访问外部变量,还要对外部变量进行修改呢?
尝试一下后会发现是不可行,objective-C 会友善的提醒你:
Variable is not assignable (missing __block type specifier)
意思是让我们加上 __block (注意是两个相连的下滑线) 关键字,在 block 内相对 block 外的变量做修改(单纯访问不用),必须对变量加上 __block 关键字。
至于 block 不能修改局部变量的原因,我们可以简单的进行一下推理,a 和 __main_block_func_0 不在一个作用域,调用过程中也只是进行的值得传递。修改 a 的值之后并不会对实际存在内存上的 a 的值有任何影响。那么为什么不用指针传递的方式,获取 a 的内存地址,来达到修改 a 的目的呢?在我上面例子里(block4),确实可以这么做,因为 main 函数在我们访问 a 地址的时候还没有完全展开,也就是说 a 的内存地址还是存在函数栈上面的。但是,block 更多情况下是用于作为参数的传递以供回调执行。当你的 block 要被执行时,你需要去访问 a 的内存地址的时候,定义所在的函数栈早就已经展开完毕销毁了,所以对于 a 的内存地址,我们是无从得知的。
1 |
|
输出结果:
block5 的源码:
带 __block 关键字变量的 block 中间的代码会比较复杂,分析起来也没有前面那么简单,我说一下自己的看法,如果有不对的地方,还望指正。
首先,__block 将我们所定义的 (int)a 封装成一个__Block_byref_a_0 结构体。在这个结构体有三个比较关键的变量:
void *__isa;
__Block_byref_a_0 *__forwarding;
定义一个指向与自己相同类型,即 __Block_byref_a_0 类型的指针。
int a;
储存目标变量 a
下面的 __main_block_impl0 函数,它的成员变量 a 变成了\_Block_byref_a_0 *类型。
通过这样看起来有点复杂的改变,我们可以修改变量i的值。但是问题同样存在:__Block_byref_a_0 类型变量 a 仍然处于栈上,当block被回调执行时,变量i所在的栈已经被展开完毕,怎么办?
在这种关键时刻,__main_block_desc_0 buibiubiu闪亮登场:
此时,main_block_desc_0多了两个成员函数:copy和dispose,分别指向main_block_copy_0和__main_block_dispose_0。
当block从栈上被copy到堆上时,会调用main_block_copy_0将block类型的成员变量i从栈上复制到堆上;而当block被释放时,相应地会调用main_block_dispose_0来释放block类型的成员变量i。
一会在栈上,一会在堆上,那如果栈上和堆上同时对该变量进行操作,怎么办?
这时候,forwarding的作用就体现出来了:当一个block变量从栈上被复制到堆上时,栈上的那个Block_byref_i_0结构体中的forwarding指针也会指向堆上的结构。
所以,以下这一段代码的结果也就不难推测
1 |
|
输出结果:
_NSConcreteMallocBlock-存储在堆上
1 |
|
输出结果:
解释
NSConcreteMallocBlock其实就是一个_NSConcreteStackBlock被copy时,将生成 _NSConcreteMallocBlock(block没有retain)。
NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现, 因为他的结构体和函数等都与被 copy 的 _NSConcreteStackBlock 一样,他们主要的区别在于一个存储在栈上,一个存储在堆上。所以我这里没有做单独的源码分析。
等等!!难道你们没有和我一开始一样感到十分震惊吗?!!
在经过copy之后,对象的类型从 NSStackBlock 变为了NSMallocBlock !!
在Objective-C的设计中,copy 一下,TM的类型都变了!不过这也说明了block 是一种特殊的对象。
block 需要 copy 的原因其实也很简单:
配置在栈上的Block也就是NSConcreteStackBlock类型的Block,如果其所属的变量作用域结束该Block就会被释放。这个时候如果继续使用该Block,就应该使用copy方法,将NSConcreteStackBlock拷贝为_NSConcreteMallocBlock。
NSMallocBlock对象再次copy,不会再产生新的对象而是对原有对象进行retain,也就是说当一个 block 为 _NSConcreteMallocBlock 它的内存管理也就相当于我们正常的对象了。即,当_NSConcreteMallocBlock的引用计数变为0,该_NSConcreteMallocBlock就会被释放。
下面是一张总结的表:
block 在 MRC 和 ARC 环境下的区别
关于 block copy
如果是非ARC环境,需要显式的执行copy或者antorelease方法。
而当ARC有效的时候,实际上大部分情况下编译器已经为我们做好了,自动的将Block从栈上复制到堆上。包括以下几个情况:
1,Block作为返回值时
类似在非ARC的时候,对返回值Block执行[[returnedBlock copy] autorelease];
2,方法的参数中传递Block时
3,Cocoa框架中方法名中还有useringBlock等时
4,GCD相关的一系列API传递Block时。
比如:[mutableAarry addObject:stackBlock];这段代码在非ARC环境下肯定有问题,而在ARC环境下方法参数中传递NSConcreteStackBlock会自动执行copy。
下面是在在网上看到的一些代码,复制过来,方便学习:
1 | //Test1 |
Test1:
exampleB_addBlockToArray添加的Block为NSConcreteStackBlock,在非ARC环境下,执行该Block时,栈上的数据已经被清除了,错误。而在ARC环境下,作为参数的Block会自动copy,不会出现问题;
Test2:
exampleC_addBlockToArray添加的Block为_NSConcreteGlobalBlock,存储在程序的数据区,相当于一个不使用外部环境的函数,没有用到栈上的数据,所有无论是ARC还是非ARC都能正常执行。
Test3
ARC中作为返回值的NSConcreteStackBlock会被执行copy操作。所有ARC运行正常,非ARC错误。
关于 block 的类型
网上有些文章会有提出说:
在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
其实仔细思考一下后,发现并非如此。这是因为本身我们常常将 block 赋值给变量,而ARC下默认的赋值操作是 strong 的,到了block身上自然就成了copy,所以常常打印出来的block就是 NSConcreteMallocBlock 了。在ARC模式下,block 三种类型也都是存在的。
关于 __block 的问题
上面说过,__block 是将对象封装为一个 Block_byref_a_0 结构来进行管理的,我们从上面的 MRC 代码也可以看出,block 根本不会对指针所指向的结构执行copy操作,而只是把指针进行的复制。(这里还要区别一下普通对象)。
不管,来一段代码:
1 |
|
输出结果:
而在 ARC 底下,对这个结构体或者说对象的管理机制就是完全遵从 ARC 的机制了,也就是自动引用计数。所以在 ARC 底下要特别注意一下, 避免循环引用。关于这个问题以及解决方法,网上已经有很多文章了,我就不再累述,推荐给大家一篇博文:iOS容易造成循环引用的三种场景,就在你我身边!
最后
不知不觉写了这么长了,关于block 的了解基本就是我以上写的着一些了。如果有不对的地方还望指正。最后,附上女神照片养下眼,码字码到眼睛要瞎了。