iOS开发 - OC - block的详解 - 深入篇
深入理解oc中的block
苹果在Mac OS X10.6 和iOS 4之后引入了block语法。这一举动对于许多OC使用者的编码风格改变很大。就我本人而言,感觉block用起来还是很爽的,但一直以来,都是知其然,而不知所以然。这篇文章一共有两篇,其中基础篇讲解了block的基本的使用和创建,以及一些注意事项。在深入篇中,我将会对block的一些原理陈述出来,探讨block的内部。
深入篇
在这篇文章中,我主要记录一些block的原理性的知识。
- 为什么说block是一个结构体,也是一个对象,同时还是携带数据的匿名函数
- 全局block ,栈block以及堆区block的区别和他们之间的联系,探究block的内存管理
- 为什么使用__block 就可以使得block可以修改外部变量
- 引起强引用循环的原因是什么,我们解决它的方法和原理又是什么
一、为什么说block是一个结构体,也是一个对象,同时还是携带数据的匿名函数
使用Mac的终端,创建一个.m文件:
vim block.m
在文件中写下以下内容:
#include<stdio.h> int main(){ void (^block)(void) = ^{ printf("a block"); }; block(); }
查看clang中间文件:
clang -rewrite-objc block.m
查看生成的.cpp文件
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) { printf("a block"); } 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(){ void (*block)(void) = ((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); }
到这里,我们可以看到,在编译之前,block复原成以上四个结构体。 它们分别是:
__main_block_impl_0
__block_impl
__main_block_desc_0
__main_block_func_0
我们暂时不管这些结构体代表的都是什么。至少知道一点是这里很好的表明了,block是结构的这个事实。仔细观察 __main_block_impl_0 的机构中,有isa指针一项(黄色标出)。看到这里理解为什么苹果强调block也是一个对象了。再看还有 __main_block_func_0 ,这里其实就是我们对block函数题的实现,它实际上是一个匿名函数,作为block的众多结构体的一部分。
所以说,block的实际含义并不像很多人所说的那么单纯,而是做一种包含了匿名函数的特殊结构体,同时它还是OC中的对象!
二、全局block ,栈block以及堆区block的区别和他们之间的联系,探究block的内存管理
在OC中,实际上有三种不同的block类型。它们分别是 全局block _NSConcreteGlobalBlock ,栈block _NSConcreteStackBlock,以及堆block _NSConcreteMallocBlock。
(1)三者如何区分
全局block
假如我创建一个下面这样block
int GlobalInt = 0; int (^getGlobalInt)(void) = ^{ return GlobalInt; };
通过clang 命令获得中间编译内容
int GlobalInt = 0; struct __getGlobalInt_block_impl_0 { struct __block_impl impl; struct __getGlobalInt_block_desc_0* Desc; __getGlobalInt_block_impl_0(void *fp, struct __getGlobalInt_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteGlobalBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static int __getGlobalInt_block_func_0(struct __getGlobalInt_block_impl_0 *__cself) { return GlobalInt; } static struct __getGlobalInt_block_desc_0 { size_t reserved; size_t Block_size; } __getGlobalInt_block_desc_0_DATA = { 0, sizeof(struct __getGlobalInt_block_impl_0)}; static __getGlobalInt_block_impl_0 __global_getGlobalInt_block_impl_0((void *)__getGlobalInt_block_func_0, &__getGlobalInt_block_desc_0_DATA); int (*getGlobalInt)(void) = ((int (*)())&__global_getGlobalInt_block_impl_0);
蓝色阴影自体中,_NSConcreteGlobalBlock 代表了这是一个全局的block。 全局block和全局变量一样,可以在整个数据域中使用。 这里的其他的任何retain、copy对它是没有影响的。它存储在静态区域,基本可以理解,在APP运行期间,它是一直存在的。 但是普通情况,我们不会经常用到这种类型的Block,因为大部分情况下我们使用block的时候都会携带上下文的环境,便于随时捕获环境的中的变量,事实上,作为全局block,它的具有其余全局变量一样的特性---可以在随处访问。然而全局block不接受所有的 retain等操作,它作为的对像属性的特点不能全部发挥出来,跟定义一个方法没有区别。
还有一种方式,像下面这样。这种方式就比较直接。我们基本可以很明显的看到,这个block是作为一个全局变量的形式被创建出来的。 还有一种更加隐秘的方式,像下面这样。
void (^block)(void) = ^{ printf("a block"); }; block(); NSLog(@"%@",block);
打印:
同样是一个全局的block。
所以全局block的生成有两种情况,一是直接将block创建成一个全局变量。这是苹果官方文档中的做法。另一种是,创建一个局部变量的block,在block的函数体内,不使用任何外部的局部变量。但是这个block和作为全局变量的block是有所不同的。 有何不同,我们将这个block的cpp中间文件打开。
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) { printf("a block"); } 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(){ void (*block)(void) = ((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); }
看红色字体,竟然是 _NSConcreteStackBlock! 也即是我们所说的栈block! 这是为什么呢? 我个人的理解是,对于CPP文件中的指示,它是告诉用户这个结构体的存储的位置。 而在nslog打印中显示不一样是因为因为没有包含局部变量,所以block本身不需要携带上下文环境,系统在编译的时候,默认Block是全局环境。 这才导致两种展示的方式不一样。
栈block
刚才说了,全局block的其中创建方式是作为一个不包含外部变量的局部变量block。 那如果这个block变量包含了外部变量那又会怎样呢。没错,当包含了外部变量的时候,它是一个栈Blobk。
在基础篇中,介绍了三种创建block的方式。其中在将block当成一个变量被创建的时候,它就是栈block。在上文中block.m 中,代码:
int a = 10; void (^block)(void) = ^{ printf("%d",a); }; block(); NSLog(@"%@",block);
这段代码中,我分别在MRC和ARC下打印:
MRC:
MRC中,是栈block。
ARC:
ARC下是堆0block。
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; } };
疑惑点:为什么ARC和MRC中的打印结果不一样呢?
正常情况下,block的申明都是在栈中的,如果需要将之转移到堆中,需要行进 block_copy或者其他发送copy的消息,比如Usingblock等。如果在MRC没有进行copy的话,那么当处于栈中的block的环境被销毁的时候,block也等同被销毁了,后续的调用将出现Crash。 而在ARC中 ,因为系统会自动对block发送copy消息,所以我们打印的时候看到block是mallco类型,即位于堆上的。在没有进行copy 之前,栈上的Block使用retain 等操作都是没有实际作用的。
如果在MRC中,我们将上面的代码更改如下,添加一条copy消息。
int a = 10; void (^block)(void) = ^{ printf("%d",a); }; [block copy]; //发送一条copy的消息 NSLog(@"%@",block);
打印结果:
堆上的block
刚才我们发现,block的申明的时候都是在栈上的,如果发送了copy消息,那么block才会被复制到堆上。
当复制到堆上之后,我们使用block就可以像使用普通的属性一样,可以进行retain等。注意,重复发送copy 消息,也只会在堆上保留一份blcok。在block所在的栈中的内容没有被销毁之前,这个栈中的block还依然存在的。但是它多了一条跟堆中block的联系。我们回过头看他的一个结构体:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("a block");
}
在这个block的结构体中,结构体本身的类型是__main_block_func_0 。 内部有一个参数指针指向了一个__main_block_impl_0,cself 结构体,这个指针实际上指向的是自己,如果block接受了copy 消息之后,那么这个指针将指向堆上的那份block,而堆上的那份的block的cself 还是指向堆上的blobk结构体,这就是为什么在复制到堆上之后,当栈上的内容被销毁时,block调用不会crash的原因了。
三、为什么使用__block 就可以使得block可以修改外部变量
在在讨论开始我们先看看当block堆外部变量使用__block修饰局部变量前后的clang吧。
使用之前
#include<stdio.h> int main(){
int a = 10; void (^block)(void) = ^{ printf("%d",a); }; block(); }
clang:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int a = __cself->a; // bound by copy printf("%d",a); }
使用之后:
#include<stdio.h> int main(){ __block int a = 10; void (^block)(void) = ^{ printf("%d",a); }; block(); }
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_a_0 *a = __cself->a; // bound by ref printf("%d",(a->__forwarding->a)); }
这里我们看到了区别。 在调用block的匿名函数的时候,如果没有使用__block是修饰的话,那么函数的参数传递方式时 将值传递,也即block内部指示获取到了变量的值而已,并没有直接获取到变量本身,那么自然也就不可能对变量进行更改了。而在使用__block修饰之后,我们发现是将变量的地址传进去了,这样在block函数体内的修改实际上就是对变量的修改。
四、引起强引用循环的原因是什么,我们解决它的方法和原理又是什么
首先看一个强引用循环的典型例子。
@interface ViewController () @property(nonatomic,assign) NSInteger intProp; /* 将block作为一个属性申明 */ @property(nonatomic,strong) TestBlock testBlock; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.testBlock = ^{ [self presentViewController:[[TestViewC alloc] init] animated:true completion:^{ }]; }; } @end
ARC 模式下会报错:
原因分析: ARC模式下,我们的常听见引用计数,一个对象,当被某个其他的对象持有的时候,会使它的饮用计数+1,如果计数为零的话,那么这个对象就会被自动销毁。这就使ARC的执行方式。保证可 以自动释放不需要的对象。
上面的代码中,block作为slef的一个属性,表明,self时持有block的,相当说:如果需要释放blcok,那么至少要做的一件事情先把self对它的持有解放,而block是self的属性,要想让slef不再持有它,只有一中情况下:self已经被释放了。 同时,我们看到block内部调用了slef,ARC会直接复制一份block到堆上去,这样,等于block会复制一份self,也即是持有了self,那么这样的情况下,如果要释放self,至少要做的一件事情是让block不再持有self,显然,上面的这种情况,要不持有slef,只能等待block被销毁。 这时候,block和slef相互等待着对方先被释放,才能释放自己,一直矛盾着两个对象都得不到释放。
好比下面的场景。
所以,所谓的强引用循环久对象得不到释放。 我们解决的办法很简单,只需要将参与引用循环的某个对象的引用设置为不持有不久解决了嘛!
有几种其他的方式可以做到。
1.OC提供了__weak修饰符,替代了这一功能。一般来说,我们会重新申请一个 weak slef 对象来参与block的copy使用。就像下面这样:
__weak ViewController *weakSelf = self;
//__weak __typeof(&*self)weakSelf =self; self.testBlock = ^{ [weakSelf presentViewController:[[TestViewC alloc] init] animated:true completion:^{ }]; };
这样警报就消失了。 很完美的感觉。
2.还看到过一篇文章,意思说当使用 __weak修饰之后,如果外部的对象为空了,那么在block的内部对象也将为空,这样有时候并不是我们想要的。 AFNetWorking中使用了一个办法解决这一问题。就是在block内部继续使用__Strong来修饰带进来的weakself, 如下:
__weak ViewController *weakSelf = self; self.testBlock = ^{ __strong __typeof(&*weakSelf) strongSelf = weakSelf; [strongSelf presentViewController:[[TestViewC alloc] init] animated:true completion:^{ }]; };
这样做的好处是,避免了强引用循环,同时保证了对象在block中的持续存在,而不会因为block外的变量因为被释放到值block的内的变量也变空了。经过尝试,确实是没问题的。
3.也有说使用__block 来避免强引用循环。 让我有点懵逼。 不过看到一篇文章对这个做了一个分析。大概意思是,使用block修饰之后,在block内部可以访问修改外部对象,而在block使用结束的时候,通过这一功能,将外部对象销毁。 避免了强引用循环,但是这样的做法有很多隐患。 我自己并没有尝试这个方法,也就不说太多了。
参考:
http://honglu.me/2015/01/06/weak与block区别/
block 中强引用与弱引用
http://blog.csdn.net/abc649395594/article/details/47086751