*带有自动变量的匿名函数。 ————引自《iOS与OS X多线程和内存管理》* 1.匿名函数 首先blocks是c语言的扩充功能 c语言中函数是这个样子的:
void func() { printf("hello world"); }那么,block是一个什么样子的呢?
^void () { printf("hello world"); }这种不带名的函数就是所谓的匿名函数 2.带自动变量 还是要从c语言函数说起。
int a = 10; void func (int b) { printf("%d + %d = %d",a,b,a+b); } int main(int argc, char * argv[]) { func(5); return 0; }上述的代码主要想说明一件事,C语言函数中,函数体中使用的函数外部变量只有两种:函数参数即全局变量。 那么block是怎么使用的呢?
int main(int argc, char * argv[]) { int a = 10; void(^block)(int) = ^(int b) { printf("%d + %d = %d\n",a,b,a+b); }; block(5); return 0; }我们看到,在此例中a已经不是全局变量了,而是一个局部变量,也就是自动变量。然而block却可以正常使用,为什么呢?因为block内部维护了一个变量a的值,所以执行正确。这里你先不用纠结,下面会有源码。由上我们就知道了什么叫做带自动变量了。
想要看Block的实质我们还是Block的实现过程。我们还是要借助clang。
#include <stdio.h> int main(int argc, char * argv[]) { int a = 10; void(^block)(int) = ^(int b) { printf("%d + %d = %d\n",a,b,a+b); }; block(5); return 0; }还是这个简单的函数,我们借助clang来转换一下。这里为了稍后方便,我们尽量删除其他无用头文件,引入必要头文件。
clang -rewrite-objc main.m转换完成后我们会发现main.m同文件夹下多了一个main.cpp的文件。打开这个文件,现在command + L跳转到第62行,复制62行至67行,command + 下调至文件底部粘贴,再跳至510行,command + shift + 上选中上面所有代码,delete删除后就剩下干货了,大概是这个样子的: 在block结构体中,三个成员变量,一个构造函数。 第一个成员变量是__block_impl的结构体,其中有block实现函数的函数指针。第二个成员变量是__main_block_desc_0,用来负责block的内存管理。第三个int型成员变量是a。 int a这个成员变量就是上面提到的带有的自动变量。因为block内部引用了外部的自动变量,所以在block结构体中多了一个同类型同名的成员变量。同样,如果没有引入外部的自动变量的话此处block结构体中也不会有这第三个成员变量。 现在将目光集中到main函数中。可以看到,第一行声明了一个局部变量,第二行调用了block的构造函数,将block对应的函数指针和Desc以及局部变量传给了函数指针指向的函数 然后我们看到,第三行调用block结构体的函数指针指向的函数,并把block自身及参数传给了函数指针指向的函数。 转过来看block指向的函数,函数首先从block自身中提取捕获的自动变量a赋值给一个临时变量,同时执行原本block的函数体。 至此就完成了一次block函数的调用过程。 这里我们要注意下捕获的自动变量: 所谓捕获的自动变量我们可以从两方面来理解: 1.我们看到在生成block的瞬间就将自动变量的值赋给了block。所以此时外界计时修改局部变量的值并不影响block中的值。 2.block中我们是不能对捕获的变量进行赋值操作的,只要这么做编译器就会警告。为什么苹果会做出这样的限制呢?因为在block里对捕获的自动变量复制其实是有歧义的。因为通过看__main_block_func_0内部的实现我们知道,block内部使用的都是block捕获到自动变量,当然这个自动变量是我们转换代码之前完全不知道的一个概念。也就是在编码过程中我们在block中使用的变量与实际代码运行过程中block内部操作的变量本就是两个变量,所以在这里修改block捕获的自动变量的值事实上跟开发者预期的结果完全是两个结果。所以苹果干脆在此就给出个警告来避免未知的错误。 3.虽说不能对捕获的自动变量进行赋值操作,但这并不影响我们使用他,否则的话这个自动变量捕获到也没有什么用了。这点很好理解,没什么好解释的。
上文说过,Block不能对其捕获的局部(非静态)变量的值进行赋值操作。既然有这些限制,那么一定有可以Block中可以做赋值操作的变量,他们都有谁呢?
静态变量全局变量__block说明符修饰的变量还是针对带有自动变量的匿名函数这句话来讲。这一节我们来探讨一下Block是如何使用外部变量的。我们知道Block截获变量的意义在于想要使用Block作用于内无法使用的变量,所以他要截获变量。接下来围绕着这句话从各种变量类型做深入的展开。 1.仅使用参数的Block
int main(int argc, char * argv[]) { void(^block)(int) = ^(int a) { a = 10; printf("block : a = %d\n",a); }; int a = 5; block(a); printf("a = %d",a); return 0; } ///clang转换后的形式 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a) { a = 10; printf("block : a = %d\n",a); } 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(int argc, char * argv[]) { void(*block)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); int a = 5; ((void (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, a); printf("a = %d",a); return 0; }由于使用的是函数的参数,是在Block作用域内可以使用的,所以Block没有对变量进行截获。这个Block基本就是最简单的函数。 2.使用局部变量(非静态)
int main(int argc, char * argv[]) { int a = 10; void(^block)() = ^() { printf("n = %d",a); }; block(); return 0; } ///clang转换后 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int a; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int a = __cself->a; // bound by copy printf("n = %d",a); } 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(int argc, char * argv[]) { int a = 10; void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); return 0; }我们看到,这里截获了这个局部变量,具体原因在上述内容中有讲到过,此处不再赘述。 3.局部静态变量 我们知道,静态变量存储在静态区,只创建一次,随后使用的同名变量均应指向同一地址。由静态变量的特性我们应该知道,如果Block截获了一个静态局域变量,并在Block中对其值进行了更改,这个操作应该是有效的,他应该改变该变量的值。我们看下他是如何实现的?
int main(int argc, char * argv[]) { static int a = 10; void(^block)() = ^() { a = 20; }; block(); printf("a = %d",a); return 0; } ///clang转换后 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int *a; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int *a = __cself->a; // bound by copy (*a) = 20; } 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(int argc, char * argv[]) { static int a = 10; void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); printf("a = %d",a); return 0; }我们看到了,Block截获的是局部静态变量的指针。这个思路跟C语言中函数一样。C语言中我们想更改实参的值时也是通过传址的方式实现的。形如:
void mySwap(int * a,int * b); int main(int argc, char * argv[]) { int a = 1; int b = 2; mySwap(&a, &b); printf("a = %d",a); return 0; } void mySwap(int * a,int * b) { int temp = *a; *a = *b; *b = temp; }4.全局变量(静态与非静态) 上面说过,Block捕获变量是为了在Block中使用其作用域外的变量,那么全局变量本身作用在区域,Block可以使用,故不需要对全局变量进行捕获。以下以全局静态变量为例。
static int a = 10; int main(int argc, char * argv[]) { void(^block)() = ^{ a = 20; }; block(); printf("a = %d",a); return 0; } ///clang 转换后 static int a = 10; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { a = 20; } 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(int argc, char * argv[]) { void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); printf("a = %d",a); return 0; }5.__block修饰的变量 我们知道,被__block修饰的局部变量,在Block内部对其进行赋值操作是可以的,那么他是如何实现的呢?
int main(int argc, char * argv[]) { __block int a = 10; void(^block)() = ^{ a = 20; }; block(); printf("%d",a); return 0; } ///clang 转换后 struct __Block_byref_a_0 { void *__isa; __Block_byref_a_0 *__forwarding; int __flags; int __size; int a; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_a_0 *a; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref (a->__forwarding->a) = 20; } static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);} static struct __main_block_desc_0 { size_t reserved; size_t Block_size; 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(int argc, char * argv[]) { __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10}; void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); printf("%d",(a.__forwarding->a)); return 0; }我们看到__block修饰的变量会自动为其生成一个结构体,并在之后对变量的操作使用的都是结构体中持有的变量a。而后Block捕获了结构体,Block中对变量的复制也映射成了对结构体内部变量的赋值。我们可以发现,在上面的例子中,不仅生成了一个__block变量的结构体,还多了__main_block_copy_0和__main_block_dispose_0两个函数,的具体作用我们稍后再表。
首先应该了解一下Block的三种类型:
_NSConcreteStackBlock///栈区Block_NSConcreteMallocBlock///堆区Block_NSConcreteGlobalBlock///全局Block我们设想这样一种情况,上述的例子中,我们看到我们都是在main函数中声明的Block,也就是说他其实block对象其实是一个局域变量,那么他一定会被存储在栈上。也就是说当出了变量的作用于,也就是main函数结束,block对象就会被销毁。这时我们的Block即为_NSConcreteStackBlock。我们可以从__block_impl结构体中的isa指针看到上述例子中的Block均为_NSConcreteStackBlock类型。
但是平时我们使用block的时候还有这么一种情况,即并不是在声明的地方立即使用,而是在等待某个时机从而进行回调。而此时一般是已经出了block对象的作用域,如果跟之前一样是栈区block的话显然block已经被销毁,此时进行回调只会引起crash。这时我们就需要_NSConcreteMallocBlock区的block了,即堆区Block。回想我们持有block的时候使用什么修饰符呢?copy对吧,而block对象执行copy操作就是将其按需复制到堆区。
#import <Foundation/Foundation.h> int globalVar = 100; void(^globalBlock)() = ^{ NSLog(@"global block beyond main"); }; int main(int argc, char * argv[]) { int a = 10; NSLog(@"stack block: %@",^{NSLog(@"here a = %d",a);}); NSLog(@"malloc block: %@",[^{NSLog(@"here a = %d",a);} copy]); NSLog(@"global block: %@",^{NSLog(@"I use nothing");}); NSLog(@"global block beyong main: %@",globalBlock); NSLog(@"global block use globalVar: %@",^{NSLog(@"%d",globalVar);}); return 0; } ///输出: stack block: <__NSStackBlock__: 0x7fff5d7cd578> malloc block: <__NSMallocBlock__: 0x60000004ef70> global block: <__NSGlobalBlock__: 0x1024320d0> global block beyong main: <__NSGlobalBlock__: 0x102432050> global block use globalVar: <__NSGlobalBlock__: 0x102432110>此处我们可以看到三种block类型。从源码我们可以知道默认生成的block均为_NSConcreteStackBlock类型,而后执行了copy操作的block为_NSConcreteMallocBlock类型,后面三个均为_NSConcreteGlobalBlock类型。 这里我们先说_NSConcreteGlobalBlock类型的Block。在全局范围内声明的Block即为全局Block,并且没有引入自动变量的也为全局Block。 现在我们知道了,调用过copy方法的block会被复制到堆区,堆区的Block均为_NSConcreteMallocBlock类型。那么什么情况下block会执行copy方法呢?
其实我们可以从上述的分析中猜到,当block需要在其作用域外使用的我们应该将其复制到堆区。例如block作为函数返回值的时候,这时候编译器会按需调用copy方法:
typedef void(^voidBlock)(); voidBlock func(); int main(int argc, char * argv[]) { NSLog(@"the return value of func:%@",func()); return 0; } voidBlock func() { int a = 10; NSLog(@"the block in func:%@",^{NSLog(@"block in func : a = %d",a);}); return ^{ NSLog(@"block in func : a = %d",a); }; } ///输出: the block in func:<__NSStackBlock__: 0x7fff56877558> the return value of func:<__NSMallocBlock__: 0x6080000486a0>此例中我们看到函数体中,输出了一个Block其为栈区Block,但是当将同样的Block作为返回值返回到main函数中的时候,他变成了堆区Block。 同学们不要说我这不是一个Block,我应该生成一个Block将其赋值,Log一下,在返回出去。这个真不是我不赋值,我不能啊,因为在ARC中赋值的时候如果不附加修饰符的话默认认为生成的变量是以__strong修饰符修饰的,而编译器遇到__strong修饰符会自动copy。。。我怎么给你做例子啊。。。反正这么写虽然不是同一个block,但是应该是同一类型block,足以说明问题。另外说过,编译器会按需调用copy方法。也就是说栈区block会出作用域销毁,全局block并不会,所以如果返回值是一个全局block的话,则不会调用copy方法。 此外以下两种情况也会由系统为我们调用copy方法:
Cocoa框架的方法且方法名中含有usingBlock等时GCD的API还有就是显示调用copy方法的时候,另外如果将其赋值给有copy修饰符修饰的属性的话也会调用copy方法。 然而什么时候应该调用copy方法呢?我们先来看下不同类型block调用copy方法会有什么行为。
Block类型 副本源的配置存储域 复制效果 _NSConcreteStackBlock 栈 从栈复制到堆 _NSConcreteGlobalBlock 程序的数据区域 什么也不做 _NSConcreteMallocBlock 堆 引用计数增加 不管Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。
————引自《iOS与OS X多线程和内存管理》
但是在我们确定的时候,还是要根据需要调用copy方法,不要盲目调用copy方法,毕竟这个方法是十分占用CPU资源的。
上文中,已经讲述了block对象在调用copy方法时候的行为。然而__block说明符修饰的变量与block对象基本一致。
正如在上文中提到的,被__block说明符的变量会自动生成一个结构体。 值得一提的是三个地方: 只有被block说明符修饰的变量,今后使用的均为其结构体中维护的同名成员变量,不过从源码中我们看到,并不是简单地使用了成员变量,而是a.forwarding->a这样一个引用方式,这是因为什么呢? 首先从__Block_byref_a_0中我们可以看到forwarding是一个Block_byref_a_0类型的结构体指针。 从main函数中第一行block变量生成的代码我们看出,在本例中生成__block变量a的同时将a的__forwarding指向了a自身。这样a.forwarding->a最终还是指向了__block变量a结构体中的成员变量a。 既然这样,就一定存在__forwarding并不指向block变量自身的情况,故此才需要__forwarding存在来保证时刻能取到一个正确的值。而上文中提到的调用copy方法的时候,就会对__forwarding指针进行操作。 由上图我们可以看到,当调用copy方法后,__forwarding指针指向堆中的__block变量。而堆中的__block变量的__forwarding指针则指向自身。
同时我们知道,block其实是对c语言的扩充,然而OC中我们使用的是引用计数来管理对象生命周期,而不是GC。所以事实上Block需要自行管理内存。那么当我们的Block捕获了一个对象时,他又是如何管理其引用计数的呢? 上文中有提到过__main_block_copy和__main_block_dispose两个函数。当Block结构体中捕获到的对象需要retain的时候则调用__main_block_copy方法增加引用计数,当其需要释放的时候则调用__main_block_dispose释放对象。所以当block从栈上复制到堆的时候会调用copy函数,而对上的block被释放时调用dispose函数。
一直以来,Block引起的循环引用都让不少初级工程师,甚至包括一些中级工程师(索性就叫他中级吧。。。)谈虎色变。他们不知道Block是如何引起循环引用的,只知道__weak可以避免循环引用。知其然不知其所以然,闹出一些笑话也是让人无语。
首先说一下什么是循环引用?
引用计数机制不做展开,我们只需要知道,在OC中对象是在引用计数为0的时候进行销毁的。一个对对象的强引用会造成一次引用计数的加一。释放一个强引用会造成引用计数的减一。 Block对内部使用的自动变量造成一个强引用,而如果这个自动变量恰好对Block也有强引用的话就会造成循环引用。 既然知道了循环引用的起因,那么我们只要打破引用的闭环就可以轻松解决。两个思路,一个是从最开始就不让强引用成为闭环,使用弱引用。另一个思路是找到一个合适的时机主动释放一个强引用,打破闭环。 1.弱引用
__weak typeof(self)weakSelf = self; self.block = ^{ NSLog(@"%@",weakSelf); }; self.block();上述代码中,使用__weak生成一个弱引用变量weakSelf,保持对self的弱引用。然后Block捕获到weakSelf,对weakSelf也是弱引用,然而却没有造成闭环。故避免了循环引用。 主动释放
__block id blockSelf = self; self.block = ^{ NSLog(@"%@",blockSelf); blockSelf = nil; }; NSLog(@"%@",self.block); self.block();上述代码中,使用__block生成一个block对象blockSelf,保持对self的强引用。然后Block捕获到blockSelf,强引用blockSelf,由于self对block还有一个强引用,此时形成了一个闭环。但当block调用的时候,内部最后将blockSelf对象置为nil。由于blockSelf置为nil,__block对象失去强引用被销毁,同时释放对self的强引用,从而打破闭环。
不过两种避免循环引用的方式都有各自的缺点。
__weak 的弱引用形式的缺点在于,当block执行的时候,由于对self是弱引用,不能保证self对象是否已经被销毁。事实上block执行前self被销毁还好,顶多是不执行。但是如果在block执行过程中,self被销毁就会造成不可预估的后果。所以当使用__weak的时候我们通常会看到如下结构:
__weak typeof(self)weakSelf = self; self.block = ^{ __strong typeof(weakSelf)strongSelf = weakSelf; NSLog(@"%@",strongSelf); }; self.block();这样的结构可以保证在block执行过程中,不会因为self释放引起问题,然而如果block执行前self被释放后block也就没有机会执行了,也算是对代码的保护。 __weak有这样的缺点,为什么不适用__block等方式呢?
事实上__block同样有着自己的烦恼,就是一定要在block体中对__block对象置为nil,且block一定要执行才可以解决循环引用。所以开发者要根据具体情况合理的选择解决循环引用的方式。