__block修饰符所干的事
我们在经常会需要在block修改外部变量,而变量是值传递的时候,我们在block里是无法修改的。
int age = 10; 这里我们声明的变量默认是auto类型。
void (^block)(void) = ^{ age = 10; }这是错误的,不能在block里修改外部的变量,block捕获外部变量,对于auto类型的局部变量,是值捕获,block里的a只是复制了a的值。我们可以去访问,但是不能修改
针对这种情况,我们在变量前加个__block 修饰符,__block int age = 10;这样就能达到在block里修改变量的目的了。
然而__block到底做了什么,起到了可以把外部值传递进来的对象在里面进行修改呢?如下图,写段测试代码
__block 修饰基础数据类型:
然后利用终端clang编译器,重新编译我们这个文件成C++文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64-arc.cpp.
在沙盒里会生成一个main-arm64-arc.cpp文件,这个文件与系统编译时,编译出来的文件是类似的,只是细节有些地方会不同。
我们可以看到 图-02 里与OC代码对应的这块,是声明的__Block_byref_age_0 对象而不是int age。然后声明block的地方,红色区域把这个变量传了进去,调用__main_block_impl_0(这个结构体便是block在C++的表现形式,block底层原理的文章有具体说明)的构造函数返回这个结构体的指针。__main_block_impl_0里如 图-03 所示,有一个__Block_byref_age_0成员,再接着看这个__Block_byref_age_0是什么东西,
我们的block结构体里会存着这个包装__block修饰的变量结构体,这个结构体是什么东西呢,如图-04所示。
这个__Block_byref_age_0里的age便是我们在外面的变量。
如何证明这个age便是外面的age呢,我们可以模拟这个文件的几个结构体,强转block对象,若如之前所说,被强转的block里面肯定有上图的几个属性。如下代码:
这里最后打印的确实是同一个地址。
如果__block修饰的是对象类型的变量,还会在__main_block_desc_0里生成两个函数,这两个函数,copy在block由栈空间搬到堆空间时被调用,disoise在block被销毁时调用。
补充1:__main_block_impl_0里对__Block_byref_age_0结构体成员在ARC环境永远下是强引用,在__Block_byref_age_0结构体里,外面捕获进来的变量如果是对象才会有强弱引用之分,在MRC情况下是弱引用,MRC编译环境的情况下,在解决循环引用的问题上,可以用__block修饰符,也能解决循环引用的问题。
补充2:在block代码块封装的函数里,访问这个外面捕获的__block所修饰的变量是通过age->__forwarding->age这样的形式,其中第一个age 是__Block_byref_age_0结构体,__forwarding是指向__Block_byref_age_0结构体自己的指针,再通过指向自己的指针访问真正age。为什么不直接通过结构体age->age呢,因为,在函数里,block默认是在栈的,但是ARC环境下,系统会默认对blockcopy搬到堆上,而block结构体里的__block变量还是在栈上,这样在堆上的__Block_byref_age_0结构访问栈上的__block变量就会存在危险,所以弄了个__forwarding指针,他是指向堆上的__Block_byref_age_0结构体,这样的话,不管这个__Block_byref_age_0是在堆上还是在栈上的,里面的__forwarding指针都是指向堆里的__Block_byref_age_0结构体自己。
2:__block修饰对象型auto变量
block捕获了对象,__main_block_impl_0结构体里的成员__main_block_desc_0结构体里就会多两个函数:copy和dipose。当block从栈上拷贝到堆上的时候,就会调用copy函数,对所有的__Block_byref_obj_1变量结构体进行copy(这里的assign是指一种策略,copy,strong,retain这种)操作,这个函数_Block_object_assign((void*)&dst->obj, (void*)src->obj,8)的第三个参数代表了是retain还是release。
用__block 修饰对象类型auto变量,同样会把捕获的对象包装成一个结构体,并且连同这个对象的修饰符一起捕获,这个结构体里会有copy和dispose两个函数用来管理对象的释放以及retain,copy等操作。
把栈上的block拷贝到堆上,__Block_byref_obj_1变量结构体也会拷贝到堆上,调用__Block_byref_obj_1变量结构体里的copy函数,copy函数里的assign函数会对外部捕获进来的__block 变量强引用或者弱引用(MRC下在这个结构体里,对捕获的变量是不会reatain,所以MRC不会出现循环引用问题)
在函数里写的block默认是栈上的,并不会对里面的对象强引用,但是ARC环境下,会对block默认进行copy操作,把block从栈上拷贝到堆上,同时也会将__main_block_impl_0结构体里所有的__Block_byref_obj_1变量结构体拷贝到堆上,堆里的__main_block_impl_0结构体对堆里的__Block_byref_obj_1结构体变量是强引用。当第二次或第三次再对着干block拷贝到堆上的就不会再堆这个__block修饰的变量拷贝,因为堆里已经有这个变量,其他拷贝的block只会指向着干__block变量(强引用)。
销毁过程
当block从堆上移除的时候,会调用dispose函数,release掉__block变量,以及捕获的变量,变量在被release的时候,如果是__Block_byref_结构体,会调用这个结构体的dispose函数。block结构体有个__forwarding指针是指向自己的,一旦被拷贝到堆上就会指向堆上的__Block_byref_结构体。这里就解决了内存在不同区的问题。
如果__block修饰的是对象类型的变量,还会在__main_block_desc_0里生成两个函数,这两个函数,copy在block由栈空间搬到堆空间时被调用,disoise在block被销毁时调用。
补充1:__main_block_impl_0里对__Block_byref_age_0结构体成员在ARC环境永远下是强引用,在__Block_byref_age_0结构体里,外面捕获进来的变量如果是对象才会有强弱引用之分,在MRC情况下是弱引用,MRC编译环境的情况下,在解决循环引用的问题上,可以用__block修饰符,也能解决循环引用的问题。
3:循环引用
Person *person =[ [Person alloc]init];
person.block = ^{NSLog(@"%@",person) };这两句代码,person里有个block变量,我们给这个block赋值,在block里打印person,这样person的block便捕获了自己,block强引用person,而block是person的属性,person也强引用着block,具体如图所示,block和person互相强引用,当person指针销毁时,person指针对person对象的强引用消失,但是block和person相互强引用,这两块内存永远被占着,造成内存泄露,引用关系如 图-3 所示。
图-03
还有种情况,比如在person的类里面,有个方法
-(void)method{ self.block = ^{NSLog(@"%@",_age) ;} }
里面没有显式的使用self,但是也会造成循环引用,OC方法里都有两个隐匿参数上面的方法代码实际是下面这个样子的,也会造成block捕获self造成强引用
- (void)method:(id)self cmd:(SEL)cmd{self.block = ^{NSLog(@"%@",_age) ;} }
__weak 和__unsafe_unretained区别:
__weak:不会产生c强引用,指向的对象销毁时,会自动让指针置为nil
__unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址不变。
总结
一.什么是block
block是把代码块封装成一个函数,以及其上环境上下文(捕获的变量)捕获的一个OC对象。同样拥有类。
二.为什么产生循环引用
一个对象持有block,block又捕获了这个对象或者这个对象的属性
循环引用引用解决办法
1.使用__weak弱指针,断开循环引用
2.当__block 修饰的变量产生循环引用时,可以在可以在block代码块里把引用的对象赋值nil。但是这种情况有个不足之处,就是block不调用,循环引用的环就一直在。
三.block捕获对象的特性
1.对于局部基础数据auto类型对象是值捕获
2.对于局部静态变量是指针捕获
3.对于对象类型是强引用或者说是连同上修饰符一起捕获
4.对于全局变量则是不捕获,可以直接使用