block的原理是怎样?本质是什么?
block本质上也是一个OC对象,block是封装了函数调用与及调用环境的OC对象,源码实例图:
另外block布局底层结构图如下
block的变量捕捉(capture)
为了保证block能够正常访问外部的变量,block有个变量捕捉机制
auto 变量捕捉
另外auto变量的捕捉是值传递,这个和static和全局变量都是不一样的,可以通过clang的源码查看auto和static的区别:
可以看到static是指针传递,而auto是值传递,有这样的差异是因为在执行函数之后age就变成了垃圾数据,所以执行block的时候,不可能去访问age的内存,但是static 的内存是一直贯穿整个运行的生命周期的?
全局变量,static全局变量,staitc局部变量
可以看到全局变量并没有捕获到block里面,但是局部的static会,因为局部变量只能在局部中访问,并且是跨域访问,所以为了能访问正确,一定需要捕捉进去,因为全局变量直接在data区域,是可以直接访问的,不需要捕获进去,循环引用也是因为基于这种捕捉情况下产生的,例如
- (void)test{
void (^block)(void) = ^{
NSLog(@"-----%p",self);
}
}
因为OC的机制,test方法会自动携带(id self,SEL _cmd)两个默认参数,所以可以当做是局部变量捕捉进去,当出现block引用self,self又引用block的时候就会导致循环引用了,上面的例子,目前这样看来当然是不会的,这个只是说明是导致循环引用的一个原理而已
对象类型的auto变量捕捉
当block内部访问了对象类型的auto变量时,如果block是在栈上,将不会auto变量产生强引用,如果block被拷贝到堆上,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据auto变量的修饰符(__strong,__weak,__unsafe_unretained)作出相应的操作,形成强引用(retain)或者弱引用,如果block从堆上移除,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的auto变量(release)
_Block_object_assign((void *)&dst->a,(void *)src->a,3/*BLOCK_FIELD_IS_BYREF*)
_Block_object_dispose((void *))src->a ,3/*BLOCK_FIELD_IS_BYREF*/);
对象类型的__block变量
_Block_object_assign((void *)&dst->p,(void *)src->p,8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void *)&src-p,8/*BLOCK_FIELD_IS_OBJECT*/);
当__block被拷贝到堆上,会调用__block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据auto变量的修饰符(__strong,__weak,__unsafe_unretained)作出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅仅限于ARC时会retain,MRC时不会retain),如果block从堆上移除,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的auto变量(release)
block的类型
block有3中类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承NSBlock类型
__NSGlobalBlock__(_NSConcreteGlobalBlock)
__NSStackBlock__(_NSConcreteStackBlock)
__NSMallocBlock__(_NSConcreteMallocBlock)
证明:
从图中我们可以看到这和上面的图片描述不一样,原因是什么呢?在解释这个问题之前,我们在看看这个例子:
从上面的图是不是可以悟出什么东西来呀,上面arc的情况下能正常打印结果,说明了arc帮我们额外做了一些操作,例如copy block从栈到堆,打印[block copy] 的值和block的地址值是一样就说明了这一点,另外从clang出来的全部都是satckblock类型的,说明llvm编译器在运行时帮我们做了一些转换类型的操作
block中的copy
ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
1.block作为函数返回值时
SLBlock myblock(){
int age = 10;
return ^{
NSLog(@"------%d",age);
}
}
终端打印结果图:
注意:如果没有age1变量的话,因为没有访问auto变量,会当做全局变量处理,打印出来是global类型
2.将block赋值给__strong指针时
int main(int argc,const char* argv[]) {
@autoreleasepool {
int age =10;
SLBlock block = ^{//默认是strong指针引用
NSLog(@"---------%d", age);
};
NSLog(@"%@", [block class]);
}
NSLog(@"%@", [block class]);
}
如果将上述的代码更改成
int age = 10
NSLog(@"%@",[^{NSLog(@"----------%d",age);} class]);
这个时候这个打印应该是stack,因为没有strong指针引用
3.block作为Cocoa API中方法名含有usingBlock的方法参数时
4.block作为GCD API的方法参数时
MRC下block属性的建议写法
@property (copy, nonatomic) void(^block)(void);
ARC下block属性的建议写法
@property (strong, nonatomic) void(^block)(void);
@property (copy, nonatomic) void(^block)(void);
__blcok 修饰符
__block 可以用于解决block内部无法修改auto变量值的问题,__block不能修饰全局变量,静态变量(static),编译器会将__block变量包装成一个对象
__block 的内存管理
当block在栈上,并不会对__block变量产生强引用,当block被copy到堆上时,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对_block变量形成强引用(retain),如下图所示:
当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose 函数会自动释放引用的_block变量(release),如下图所示:
从clang出来的源码中,我们可以发现里面__block修饰的会有一个__forwarding指针,这个指针是干嘛用的呢?然而这里__forwarding指针真的永远指向自己么?我们来做一个实验
可以看到MRC环境下,var->forwarding->var(age的指针)是相同的,说明栈中的block并没有拷贝到堆中,并且此时的forwarding指向的就是自己栈上block的内存地址,再看一下这个情形:
可以看到MRC下block使用copy,和ARC下直接定义是一样的情况,说明ARC下,自动帮我们将strong 引用的block拷贝到了堆中,这个时候,可以发现两个age的地址值并不一样,由此可以说明_forwarding 指针指向的不是同一个并且由打印结果age的值可以看到,block里面age的值和block外age的值是一样,都是30,所以在使用__block之后,在栈中访问age的时候,val->forwarding->val 访问的也是堆中的age值,在堆中访问age的时候,val->forwarding->val 访问的是堆中的age值,那这个说明了forwarding是为了让我们更好的管理内存的,不论现在block是出于栈中还是堆中,都不会影响到寻找到的相关信息,当block是在栈中,__forwarding指向的就是栈本身的地址,当block copy到堆中的时候,__forwarding指针指向的就是堆本身的地址,如下图所示:
上面中有没有人存在一个这样的问题,就是age的地址值,到底是__Block_byref_age_0 *age 的地址值,还是__Block_byref_age_0 *age 里面的变量age的地址值呢?我们来证明一下:
__weak问题
在使用clang转换OC为C++代码时,可能会遇到以下问题
cannot create __weak reference in file using manual reference
解决方案:支持ARC,指定运行时系统版本,比如:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp
循环引用
例子1:
可以看到在打印之前,person因为出了作用域,应该被销毁才对的,但是这里斌没有执行dealloc操作,说明person没有被释放,内存泄漏了,我们来分析一下内存分布图:
可以看到运行到大括号外面的时候,箭头1销毁,但是这个时候2,3还是相互引用,导致不能释放,所以出现了循环引用,这个时候应该怎么解决呢?直接让其中一根线是弱引用就可以了,3弱引用也可以,2弱引用也可以,因为这里block是person的一个属性,所以想person销毁的时候,block就销毁,那么我这里就是让block里面的引用弄成弱引用就可以了,例如下图所示:
相应的代码更改成为:
大括号结束后,引用1销毁,person没有强引用指向,person销毁,person销毁之后,因为3是若引用,所以block销毁,内存循环应用解除,这里除了用__weak 之外,使用__unsafe__unretained也不会产生强引用,但是不安全,指向的对象销毁之后,指针存储的地址不变,再次访问的时候会导致野指针,__weak会赋值成nil,安全;当然通过__block 也可以,具体可以看下面的补充要点;
补充要点
1.block的属性修饰词为什么是copy?使用block有哪些使用注意?
block一旦没有进行copy操作,就不会在堆上,需要注意循环引用问题,例如下图展示:
使用ARC解决循环引用
使用MRC解决循环引用
MRC情况下,如果不加__unsafe_unretained ,或者__block 修饰的话,如果block使用了copy拷贝到堆上的时候,因为声明的变量是强引用,在内部会对变量使用retain一次,这样最后就会导致强引用了,也就是会循环引用
可以添加微信一起交流学习:fslskz