探索底层原理,积累从点滴做起。大家好,我是Mars。
往期回顾
iOS底层原理探索—OC对象的本质
iOS底层原理探索—class的本质
iOS底层原理探索—KVO的本质
iOS底层原理探索— KVC的本质
iOS底层原理探索— Category的本质(一)
iOS底层原理探索— Category的本质(二)
iOS底层原理探索— 关联对象的本质
iOS底层原理探索— block的本质(一)
今天继续带领大家探索iOS之block
的本质。
block内修改变量的值
首先我们来看一段代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void(^block)(void) = ^ {
a = 20;
NSLog(@"a = %d",a );
};
block();
}
return 0;
}
代码中变量a
的值会改变吗?答案是不会,这段代码编译的时候就会报错。因为我们无法在block
内部直接修改变量的值。
变量a
是在main
函数中声明的,说明变量a
的内存在main
函数的栈空间内存储。我们在iOS底层原理探索— block的本质(一)中分析过block
的底层结构:block
块内的执行代码是在底层中的__main_block_func_0
函数中。__main_block_func_0
函数内部的a
是block
结构体中的a
,main
函数和__main_block_func_0
函数的栈空间不一样,因此无法在__main_block_func_0
函数中去修改main
函数内部的变量。
那么我们就真的无法修改block
内部变量的值了吗?当然不是,系统为我们提供可两种方式来完成对block
内部变量的值进行修改。
1.使用static关键字
我们在iOS底层原理探索— block的本质(一)中block
的变量捕获机制提到过:对static
修饰的变量,block
以指针访问的形式捕获变量。所以我们用在static
修饰了变量a
后,就可以在__main_block_func_0
函数内部直接拿到变量a
的内存地址,因此我们也就可以在block
内部修改变量a
的值了。
2.使用__block关键字
__block
用于解决block
内部不能修改变量值的问题。编译器会将__block
修饰的变量包装成一个对象
注意:__block
不能修饰全局变量、静态变量(static
)
经过测试验证,
__block
内部可以修改用
__block
关键字修饰的变量的值。那么
__block
都做了些什么呢?我们看一下底层代码:
可以看到,经
__block
关键字修饰后,变量
a
在
block
底层结构体
__main_block_impl_0
中以
__Block_byref_a_0
类型的结构体被捕获。
我们看一下在定义变量a
时,经__block
关键字修饰后有什么不同。来到main
函数中:
从红色标注的代码可以看到,经过__block
关键字修饰后,变量a
为__Block_byref_a_0
类型,并且传入了5个参数。这5个参数分别对应的传入了上图中__Block_byref_a_0
类型的结构体中。我们来分析一下__Block_byref_a_0
结构体中的5个元素:
__isa
指针 :结构体中存在的这个isa
指针也就说明了__Block_byref_a_0
本质是一个对象。
__forwarding
:__forwarding
是__Block_byref_a_0
结构体类型的,并且__forwarding
存储的值为(__Block_byref_a_0 *)&a
,即结构体自己的内存地址。
__flags
:0
__size
:sizeof(__Block_byref_a_0)即__Block_byref_a_0所占用的内存空间。
a
:用来存储变量,这里存储10。
上图的代码中,在定义block
时,将__Block_byref_a_0
类型的变量a
的地址传进去(黄色标注)。我们上文讲了,__main_block_impl_0
结构体中有一个__Block_byref_a_0 *a
,它存储的就是传进来的变量a
的地址。
接下来我们看一下block
内修改变量值和打印变量值的代码,转换后即__main_block_func_0
函数的代码:
首先先拿到
__main_block_impl_0
中的
__Block_byref_a_0
类型的
a
结构体,然后就是红色标注的代码,这句代码的意思是:
通过a
结构体拿到__forwarding
指针,再通过__forwarding
拿到结构体中的变量a
并赋值为20。上文讲过
__forwarding
中保存的就是
__Block_byref_a_0
结构体本身的地址,这里也就是
a
的地址)。
同样NSLog
打印a
的值也是通过同样的方式获取a
的值完成打印,当然此时的a
的值已经是20了。
至此,我们就弄清楚了为什么经过__block
关键字修饰后就修改变量的值了。__block
将变量包装成对象,然后再把变量封装在结构体里面,block
内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。
block的内存管理
如果__block
修饰对象类型的变量,能否在block
内部改变这个变量呢?我们写一段代码测试一下:
通过打印输出null
证明对象类型的变量经过__block
修饰后,在block
内部也可以进行修改。那么这时候block
底层结构体是什么样子呢?
我们在iOS底层原理探索— block的本质(一)中提到过:当block中捕获对象类型的变量时,block
底层结构中__main_block_desc_0
结构体内部会自动添加copy
函数和dispose
函数对捕获的变量进行内存管理。
我们还发现在底层结构中,跟上文一样,经过
__block
修饰后的变量被
block
捕获后,是将对象包装在一个新的结构体
__Block_byref_person_0
中。不一样的地方是结构体内添加了内存管理的两个函数
__Block_byref_id_object_copy
和
__Block_byref_id_object_dispose
我们来回顾一下在上一篇文章中讲到的block
内部的内存管理:当blcok
被执行copy
操作,复制到堆区时,会调用block
内部的copy
函数,copy
函数内部会调用_Block_object_assign
函数,_Block_object_assign
函数会对变量进行强引用。
当block
被copy
到堆上时,block
内部引用的变量也会被复制到堆上,并且持有变量。如下图所示:
当
block
从堆中移除时,就会调用
dispose
函数,也就是
__main_block_dispose_0
函数,
__main_block_dispose_0
函数内部会调用
_Block_object_dispose
函数,会自动释放引用的变量。
上文讲到,block
内部捕获经__block
关键字修饰的变量a
,在block
底层结构体__main_block_impl_0
中的__Block_byref_a_0
类型的结构体存在__Block_byref_a_0
类型的指针__forwarding
。__forwarding
指针指向自己本身,即__Block_byref_age_0
结构体。
__forwarding
的作用是:当block
被复制到堆中时,栈中的__Block_byref_age_0
结构体也会被复制到堆中,而此时栈中的__Block_byref_age_0
结构体中的__forwarding
指针指向的就是堆中的__Block_byref_age_0
结构体,堆中__Block_byref_age_0
结构体内的__forwarding
指针依然指向自己。
所以当我们修改变量时,系统会通过
__forwarding
指针将修改的变量赋值在堆中的__Block_byref_age_0中。
我们继续分析上文讲到的__block
修饰的对象类型变量在底层结构体中新增加的两个函数:void (*__Block_byref_id_object_copy)(void*, void*);
和void (*__Block_byref_id_object_dispose)(void*);
。这两个函数为__block
修饰的对象提供了内存管理的操作。void (*__Block_byref_id_object_copy)(void*, void*);
和void (*__Block_byref_id_object_dispose)(void*);
赋值的分别为__Block_byref_id_object_copy_131
和__Block_byref_id_object_dispose_131
首先我们看一下这两个函数的实现:
从源码中可以发现
__Block_byref_id_object_copy_131
函数中同样调用了
_Block_object_assign
函数,而
_Block_object_assign
函数内部拿到
dst
指针即
block
对象自己的地址值加上40个字节。并且
_Block_object_assign
最后传入的参数是131。
我们计算一下
__Block_byref_person_0
结构体占用内存空间大小:
__Block_byref_person_0
结构体占用的空间为48个字节。而在
__main_block_copy_0
函数中传入的参数中有参数8,8+40=48,那么说明
__Block_byref_id_object_copy_131
函数传入的参数
dst
就是
__Block_byref_person_0
结构体,也就是我们声明的
person
对象的指针。
那么我们可以得出结论:
copy
函数会将person
地址传入_Block_object_assign
函数,_Block_object_assign
中对person
对象进行强引用或者弱引用。
当然,当block
从堆中移除时,会调用dispose
函数,block
中放弃对__Block_byref_person_0
结构体的引用,__Block_byref_person_0
结构体中也会调用dispose
操作放弃对person
对象的引用,保证结构体和结构体内部的对象可以正常释放。
block的循环引用问题
我们先来看一段代码,声明一个Person
类,有age
和block
两个属性,在Person
类的dealloc
方法中打印方法名。并在main
函数中编写:
这段代码有没有问题呢?
答案是有问题,打印只会输出
18
和
程序执行完毕
,并没有打印
Person
类的
dealloc
方法名。也就是说,我们在
main
函数中声明的
person
对象没有被释放。
原因就是这段代码产生了循环引用。person
对象强引用着block
,block
内部强引用了person
对象,程序结束时,两者都不会被释放,这就造成了内存泄漏。
系统为我们提供了几种方案来解决循环引用问题:
1、__weak
__weak
不会产生强引用,指向的对象销毁时,会自动将指针置为nil
。
使用__weak
修饰person
对象,block
内部将指针变为弱指针。block
对person
对象为弱引用,就不会出现相互引用、不会被释放了。
2、__unsafe_unretained
在MRC环境下,是不支持__weak
的,可以用__unsafe_unretained
来修饰对象。
__unsafe_unretained
同样不会产生强引用,但是指向的对象销毁时,指针存储的地址值不变,所以不安全。
3、__block
__block
同样也可以解决循环引用问题。前提是必须调用执行block
,并且在block
内部手动将person
对象置为nil
。
原因就是在__block
修饰了person
对象之后,block
内部引用的person
对象其实是底层结构体中__Block_byref_person_0
内部的person
对象,那么当person
对象置为nil
也就断开了__Block_byref_person_0
结构体对person
的强引用,从而循环引用也就不存在了。
结语
至此,我们通过两篇文章的研究完成了对block
的底层探索,相信大家对block
的底层也有了一定的掌握。文中很多例子大家自己亲手实践一遍,会对底层有一个更深刻的理解。如果你在阅读或者测试过程中遇到什么问题,欢迎来留言交流!
更多技术知识请关注公众号
iOS进阶