一、Block的底层结构及本质
(1)block本质:
从代码可以看出,Block的本质就是NSObject. 也就是说block就是一个对象。
(2)block结构
利用clang将main.m文件编译为cpp文件。看下block底层实现的结构
最终block是转成了__main_block_impl_0的结构。
block结构体表示如下:
Block结构二、Block的变量capture机制
capture机制:为了保证block内部能够正常访问外部的变量。
1、局部变量捕获:(会捕获到block内部成为新的结构体变量成员)
(1)auto类型的直接捕获到block内部(值传递)。
证明:
再看下impl结构体的变化
可以看到,age值进过构造函数直接值传递到impl结构体里面的age。
(2)static 变量捕获是指针传递
证明:
2、全局变量捕获
(1)全局变量在block里面是直接访问。(不会捕获到block内部产生新的结构体变量成员)
证明:
从截图可以看到,age在nslog打印时(block里面的任务被包装成函数),是直接访问拿来用的,并没有捕获进到block内部。
(3)同理,全局的obj对象,也是不会捕获到block里面,如果是全局NSObject*obj对象就直接访问NSObject*obj,如果是static NSObject*obj对象,就是会把NSObject**obj作为传递进去使用(&obj传进去)。(二级指针)
三、Block的类型
Block类型有:__NSGlobalBlock__、__NSStackBlock__、__NSMallocBlock__最终都是继承自NSBlock类型,基类是NSObject。(可查看最上面的截图)
1、__NSGlobalBlock__(存放在数据区):没有访问auto变量的block就是__NSGlobalBlock__;
2、__NSStackBlock__(存放在栈段):访问了auto变量的block就是__NSStackBlock__;
3、__NSMallocBlock__(存放堆段,需要程序猿管理):__NSStackBlock__调用copy后得到block就是__NSMallocBlock__;
(1)ARC环境下证明
(2)MRC环境下证明
如果block2赋值的时候加上copy,则block会变成mallocBlock。
说明:
(1)ARC环境下,编译器会根据情况自动将栈上的block复制到堆上。比如:
1、block作为函数返回值时
2、将block赋值给__strong指针时
3、block作为方法名含有usingBlock的方法参数时
4、block作为GCD API的方法参数时
(2)ARC下block属性的建议写法:
1、@property(strong, nonatomic) void(^block)(void);
2、@property(copy, nonatomic) void(^block)(void);
(3)MRC下block属性的建议写法:@property(copy, nonatomic) void(^block)(void);
四、block内部有对象类型的auto变量,内存管理注意点
1、当block内部访问了对象类型的auto变量时:
先看底层结构:
main函数代码如下
转cpp代码:
结构体多了person指针结构体desc里面多了两个函数指针
copy与dispose函数的作用:对block捕获的auto对象变量做内存管理,retain/release等操作,具体如果操作根据auto对象变量修饰符的方式进行。
copy及dispose函数具体实现如下:
(1)如果block是在栈上,将不会对auto变量产生强引用(or retain)
MRC证明:
ARC证明:
(2) 如果block被拷贝到堆上
1、会调用block内部的copy函数
2、copy函数内部会调用_Block_object_assign函数
3、_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
4、如果block从堆上移除会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的auto变量(release)
说明:block捕获__weak与__strong的auto变量对象,学到这里就需要知道为什么_strong的会导致强引用了。(默认不修饰auto对象变量,隐藏了修饰符__strong。)
五、__block修饰符
可以用于解决block内部无法修改auto变量值的问题。
block捕获进去的auto变量,是不允许被修改的。因为block的实现内容也是被包装成一个函数的调用。函数与函数直接是不能跨域访问auto变量的,auto变量属于对应的函数栈空间内。
block可以修改全局变量跟static变量。
那我们来看看给auto变量加上__block后,为什么就可以修改变量了。
main函数的代码如下:
转cpp后,看下__block int age的变化
从底层本质看出,不再是一个简单的int age变量,而是经过编译器转换成了对象(结构体__Block_byref_age_0),结构体里面存放着int age这个成员(从这大概可以知道,为了改变int age的值,先包装成一个对象,利用结构体指针来修改成员int age值)。
然后在block内部修改age的值,是通过age结构体指针去修改结构体成员age的值。以此达到了我们修改age值的目的。
特别说明:
1、__block int abc基本数据类型格式的捕获
(1)如果block此时是在stack,属于stackBlock,此时block里面捕获__block int abc变量,此时结构体(__Block_byref_abc_0)是在栈中,当然结构体的age成员值也是在栈中。
证明:
特别说明:此处打印的abc地址是__Block_byref_abc_0结构体里面的成员int abc的地址。不是__Block_byref_abc_0结构体的地址。但是也可以说明__Block_byref_abc_0结构体目前是存在栈中。
(2)如果block进过copy,生成的mallocBlock,里面捕获__block int abc变量,此时__Block_byref_abc_0结构体也会被copy在堆上。
证明如下:
小tips: 因为有个全局的block1强引用着block,也就意味着在其他函数中也可以用的这个block,所以__block int age也必须copy到堆中才行,这样才能保证全局block1(30)调用,可以修改abc的值。如果abc还在栈中,可能早就被其他数据覆盖或者函数执行完后成为了垃圾数据。
(3)struct__main_block_impl_0结构体里面的copy及dispose函数管理由__block int abc生转换成的__Block_byref_abc_0的内存。
编译cpp后得到底层代码如下:
管理结构体__Block_byref_abc_0内存函数2、__block NSObject*obj对象类型的捕获
(1)同样生成__Block_byref_obj_1的结构体
与之前的__block int age不同的是,这个结构体里面多了两个函数copy,dispose.这个两个函数用来对结构体里面的obj对象成员做内存管理。
3、__block的内存管理
(1)当block在栈上时,并不会对__block变量产生强引用
MRC环境证明:
ARC环境证明:
(2)当block被copy到堆时,会调用block内部的copy函数(copy函数内部会调用_Block_object_assign函数),_Block_object_assign函数会对__block auto变量形成强引用(retain),也会将__block auto变量拷贝到堆上。
ARC证明
(3)特殊请看,block被copy到堆时,捕获的__block obj局部auto对象MRC情况下有点特别,不会被retain
请看下面代码
内存管理指示图
(4)当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的__block变量(release)
(5)__block的__forwarding指针妙用:当我们的block复制到堆中时,__block变量也会复制到堆中,这时在栈中的__block变量和堆中的__block变量的__forwarding指针都会指向堆中的__block变量。这样做的目的:即使你修改栈中的block内部的__block变量,也能达到修改到堆中__block变量,防止程序修改出错。
六、解决循环引用
1、ARC环境
第三种利用__block打破循环示意图
2、MRC环境
六、总结
1、Block本质是OC对象,基类也是NSObject
2、Block分为:__NSGlobalBlock__、__NSStackBlock__、__NSMallocBlock__三种类型
3、Block捕获auto变量(假设int age),会在Block结构体里面生成对应成员int age
4、Block不会对全局变量(假设int tall)捕获,在block里面直接可以访问全局int tall,不会被捕获
5、Block捕获局部static变量(假设static int height),会在Block结构体里面生成对应成员int *height(指针)
6、当block在栈上时,对(对象类型的auto变量、__block变量)都不会产生强引用
7、当block拷贝到堆上时,都会通过copy函数来处理它们
(1)__block变量(假设变量名叫做a) 调用如下函数
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
(2)对象类型的auto变量(假设变量名叫做obj) 调用如下函数
_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
8、当block从堆上移除时,都会通过dispose函数来释放它们
(1)__block变量(假设变量名叫做a) 调用如下函数
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
(2)对象类型的auto变量(假设变量名叫做obj)
_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
9、当__block变量被copy到堆时,会调用__block变量内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)
10、如果__block变量从堆上移除会调用__block变量内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放指向的对象(release)
11、ARC解决循环引用:(1)用__weak、__unsafe_unretained解决(2)用__block解决(必须要调用block)
12、MRC解决循环引用:(1)__unsafe_unretained (2)__block
网友评论