iOS进阶iOS 移动端开发

iOS底层原理探索— block的本质(二)

2019-07-29  本文已影响15人  劳模007_Mars

探索底层原理,积累从点滴做起。大家好,我是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函数内部的ablock结构体中的amain函数和__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的值了。

static修饰变量.png

2.使用__block关键字

__block用于解决block内部不能修改变量值的问题。编译器会将__block修饰的变量包装成一个对象
注意:__block不能修饰全局变量、静态变量(static)

__block修饰变量.png
经过测试验证,__block内部可以修改用__block关键字修饰的变量的值。那么__block都做了些什么呢?我们看一下底层代码:
__block修饰变量的c++代码.png
可以看到,经__block关键字修饰后,变量ablock底层结构体__main_block_impl_0中以__Block_byref_a_0类型的结构体被捕获。

我们看一下在定义变量a时,经__block关键字修饰后有什么不同。来到main函数中:

main函数代码.png

从红色标注的代码可以看到,经过__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函数的代码:

block内封装的执行函数.png
首先先拿到__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内部改变这个变量呢?我们写一段代码测试一下:

__block修饰对象类型变量.png

通过打印输出null证明对象类型的变量经过__block修饰后,在block内部也可以进行修改。那么这时候block底层结构体是什么样子呢?
我们在iOS底层原理探索— block的本质(一)中提到过:当block中捕获对象类型的变量时,block底层结构中__main_block_desc_0结构体内部会自动添加copy函数和dispose函数对捕获的变量进行内存管理

__block修饰对象类型变量后的__main_block_desc_0.png
我们还发现在底层结构中,跟上文一样,经过__block修饰后的变量被block捕获后,是将对象包装在一个新的结构体__Block_byref_person_0中。不一样的地方是结构体内添加了内存管理的两个函数__Block_byref_id_object_copy__Block_byref_id_object_dispose
__block修饰对象类型变量后block底层结构.png

我们来回顾一下在上一篇文章中讲到的block内部的内存管理:当blcok被执行copy操作,复制到堆区时,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对变量进行强引用。

blockcopy到堆上时,block内部引用的变量也会被复制到堆上,并且持有变量。如下图所示:

block执行copy操作.png
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指针指向.png

__forwarding的作用是:当block被复制到堆中时,栈中的__Block_byref_age_0结构体也会被复制到堆中,而此时栈中的__Block_byref_age_0结构体中的__forwarding指针指向的就是堆中的__Block_byref_age_0结构体,堆中__Block_byref_age_0结构体内的__forwarding指针依然指向自己。

block复制到堆后__forwarding指针指向.png
所以当我们修改变量时,系统会通过__forwarding指针将修改的变量赋值在堆中的__Block_byref_age_0中。
block内部修改变量的代码.png

我们继续分析上文讲到的__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修饰的对象类型变量的内存管理函数.png
从源码中可以发现__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结构体占用内存.png
__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类,有ageblock两个属性,在Person类的dealloc方法中打印方法名。并在main函数中编写:

循环引用测试代码.png
这段代码有没有问题呢?
答案是有问题,打印只会输出18程序执行完毕,并没有打印Person类的dealloc方法名。也就是说,我们在main函数中声明的person对象没有被释放。

原因就是这段代码产生了循环引用。person对象强引用着blockblock内部强引用了person对象,程序结束时,两者都不会被释放,这就造成了内存泄漏。

系统为我们提供了几种方案来解决循环引用问题:

1、__weak

__weak不会产生强引用,指向的对象销毁时,会自动将指针置为nil

使用__weak修饰person对象,block内部将指针变为弱指针。blockperson对象为弱引用,就不会出现相互引用、不会被释放了。

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进阶


iOS进阶.jpg
上一篇下一篇

猜你喜欢

热点阅读