首先,我们看两段代码:
从运行结果可以看出,如果是普通局部变量age,第17行和第22行的age地址是一样的,第20行的地址跟前面两个是不同的。
这个原因我们在上节已经分析过。是因为:
第17和第22行的age是age的地址
第20行的age是捕获进去的age,其是一个在block内部新建的同名age,因此,地址不同。
转化为底层代码可以看到:
从底层代码可以看出,三者最后都是取的&(age.__forwarding->age),也就是被包装为__Block_byref_age_0类型后里面的age。可以解释2—和3—地址一样。
按说1—的地址应该也是一样的,不知为何1—的地址不同
上面两端代码都是在ARC环境下进行的,是不是ARC又为我们做了哪些操作,从而造成这样的结果呢?
我们分别将两段代码运行在MRC上
这个结果跟在ARC上的结果是一致的,不再谈论
这个结果就跟我们跟在ARC上的结果是不同的,同样,我们看下其底层源码:
源码也一样,怎么结果就是不一样呢?到底ARC为我们做了什么?
我们继续做实验
在ARC下,我们添加了几个打印,可以看出在ARC下,blcok的类型是NSMallocBlock,存储在堆上的。
修改为MRC后:
不一样的地方出现了,在MRC下block的类型是NSStackBlock类型的,存储在栈上。
按照我们之前的结论:
访问auto变量的block是NSStackBlock类型的。
将^{}block赋值给强指针YZBlockL类型的block,也就是 ^{}block有强指针引用,因此,在ARC下 ^{}block 会自动将栈上的block复制到堆上,所以,block的类型是NSMallocBlock。
也就三个打印的age都是&(age.__forwarding->age)的地址。
但是1—是在栈上的age。2—和3—都是访问的堆上面的age。
上面的代码,也可以看出,使用__block修饰的age变量,底层的时候,也调用了copy和dispose相关函数,并在copy函数里面通过__Block_object_assign使用&dst->age,访问age,并对age进行强引用操作。而&dst就是__main_block_impl_0,其里面的age就是__Block_byref_age_0 *age。
换句话说,就是使用_block修饰的变量,底层会进行copy操作,并在copy操作中对里面转换为__Block_byref_age_0类型的age进行相应的内存管理。
__block的内存管理
当block在栈上时,并不会对__block变量产生强引用
当block被copy到堆上时
会调用block内部的copy函数
copy函数内部会调用__Block_object_assign函数
__Block_object_assign函数会对__block修饰的变量进行内存管理
这个图表明,在进行copy操作后,block会从栈复制到堆上面。同时,会将block内部访问的使用__block修饰的变量也复制到堆上面,并且,堆上面的block还是会对堆上面的__block修饰的变量进行引用关系。
如果,有两个block,同时引用一个__block修饰的变量呢?
两个block同时引用一个__block修饰的变量,会对__block进行相当于+2的操作
运行结果
2020-04-28 16:55:09.859862+0800 block学习[5641:192209] 1----0x7ffeefbff4f8
2020-04-28 16:55:09.860258+0800 block学习[5641:192209] 2----0x100703b48
2020-04-28 16:55:09.860365+0800 block学习[5641:192209] ---30---
2020-04-28 16:55:09.860406+0800 block学习[5641:192209] 3----0x100703b48
2020-04-28 16:55:09.860437+0800 block学习[5641:192209] ---30---
2020-04-28 16:55:09.860465+0800 block学习[5641:192209] 4----0x100703b48
可以看到2、3、4的age都是同一个内存地址。
上面两个图可以看到,一个是__main_block_impl_0类型,一个是__main_block_impe_1类型,但是从定义可以看出
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
里面的age统一都是__Block_byref_age_0类型,因此,上图里面&dst->age访问的age都是同一个,是__Block_byref_age_0类型里面的age。
当block从堆中移除的时
会调用block内部的dispose函数
dispose函数内部会调用__Block_object_dispose函数
__Block_object_dispose函数会自动释放引用的__block修饰的变量
__block的__forwarding指针
在__block修饰变量的几次例子中,我们都看到,最后访问里面的age是使用age.__forwarding->age访问的方式,这是为什么呢?
首先,我们知道__forwarding指向的就是他自己,那么, 为什么不直接访问里面的age变量,而是通过__forwarding指针访问age变量呢?
这是因为,如果我们访问的age变量在栈上的时候,通过__forwarding找到它自己,然后访问里面的age,很容易找到age,这是行的通的,虽然复杂了一步。
但是,如果block已经复制到堆上面,__forwarding的作用就突显出来了。__forwarding指针会指向堆上面的block,这样里面访问的__block修饰的变量,确保了是堆上面的。
注:复制并不是剪切,栈上面是还有的。
前面我们讲的是__block修饰基础变量类型,如果用__block修饰对象类型的变量,将会是怎样的情形呢?
__block修饰对象类型
代码通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 main.m
指令,转换为底层代码
部分代码为:
可以看出,使用__block修饰的对象类型,也会生成一个__Block_byref_person_0类型且变量名为person的变量。在__Block_byref_person_0定义里面,有一个YZPerson类型且变量名为person的变量。
首先,我们看到这个person是__strong修饰的。
那么,如果使用__weak修饰对象的话,会是怎样的情况呢?
首先,我们要明确一点,__weak是修饰person对象的,而跟__block无关。这也就解释了为何__block __weak int age = 10;会有一个警告。因为__weak只能修饰对象。
转换为底层代码可以看到:
可以看到,__weak是修饰的YZPerson类型的person对象,而跟__Block_byref_weakPerson_0类型无关。
也就是__block修饰的对象生成的__Block_byref_weakPerson_0类型,都是强引用。而__Block_byref_weakPerson_0里面的YZPerson类型的person对象是强还是弱,跟外部使用的强或弱有关。
继续研究发现
__Block_byref_weakPerson_0内部也有一个__Block_byref_id_object_copy和__Block_byref_id_object_dispose函数。这是使用__block修饰基本数据类型所没有的两个新东西。
那么,这两个新东西是什么呢?
我们知道,当block被复制到堆上时,__block修饰的变量也会被拷贝到堆上面去,当__block修饰的变量被拷贝到堆上时,内部就会调用上图两个函数,对其进行内存管理相关操作。
在对__block修饰的变量进行转换的时候,我们可以看到:
__block __weak YZPerson *weakPerson = person;
转换为:
__attribute__((__blocks__(byref))) __attribute__((objc_ownership(weak))) __Block_byref_weakPerson_0 weakPerson = {
(void*)0,
(__Block_byref_weakPerson_0 *)&weakPerson,
33554432,
sizeof(__Block_byref_weakPerson_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
person
};
对应
struct __Block_byref_weakPerson_0 {
void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
YZPerson *__weak weakPerson;
};
可以看到__Block_byref_id_object_copy_131传给了 void (* __Block_byref_id_object_copy)(void, void);
__Block_byref_id_object_dispose_131, 传给了 void (* __Block_byref_id_object_dispose)(void*);
通过定义可以看到:
可以看到,其内部也有一个__Block_object_assign或__Block_object_dispose函数。并且里面有一个dst+40的操作,dst+40后是谁呢?
可以看出,dst+40正好指向的是YZPerson类型的weakPerson对象。这样就看出,__Block_byref_id_object_copy和__Block_byref_id_object_dispose是对YZPerson类型的weakPerson对象进行内存管理的。
当__block变量在栈上时,不会对指向的对象产生强引用
当__block变量被copy到堆时
会调用__block变量内部的copy函数
copy函数内部会调用__Block_object_assign函数
__Block_object_assign函数会根据所指向对象的修饰符(__strong\__weak\__unsafe_unretain)做出相应的操作,形成强引用或弱引用(注意:ARC才会强或弱,MRC只会是弱)
如果__block变量从堆上移除
会调用__block变量内部的dispose函数
dispose函数内部会调用__Block_object_dispose函数
__Block_object_dispose函数会自动释放指向的对象
关于上面注意里面的话,我们可以举一个在MRC下的例子:
在23行,block还没释放,person就已经被释放了,这说明:
在MRC下,__block后面修饰的变量是强或者弱都没有关系,最后block对__block修饰的变量都是弱引用。
block的循环引用问题
首先,看一段代码
可以看到,在19行结束后,也没有打印person消失的信息,说明person没有消失,被循环引用了。
大致就是,block内部其实是有一个强指针引用了YZPerson类型的person对象,指向着YZPerson。而YZPerson对象里面有一个_block的成员变量,指向着对应的block,这就产生了循环引用问题。
左上角的block是指的16行等号右边的block
右边的YZPerson(MJPerson)是指的14行等号右边的东西
左下角的person是指的14行左边的内容
在ARC下解决循环引用一般使用__weak或者__unsafe_unretained
两者的区别是:
__weak会在指向的对象消失的时候,将指针置为nil;
__unsafe_unretained会在指向的对象消失的时候,不会将指针置为nil;
当然,也可以借助__block来解决循环引用问题
如果没有person = nil的情况下,其各种关系是如下:
__block指的是:__block修饰的person
对象指的是:(__block修饰的person)里面的YZPerson类型的person对象
Block指的是:person.block(),是YZPerson类型的person对象拥有的成员变量
Block持有__block,是指的16行代码,等号右边的内容
加上一句置为nil的操作后,其关系为:
在MRC下,可以使用__unsafe_unretained解决循环引用。因为MRC没有__weak,也就不存在使用__weak解决循环引用的问题了。
使用__unsafe_unretained可以解决的原因是,block内部引用对象的时候,不会对引用计数器+1。
另外,我们也可以直接使用__block解决循环引用问题。原因的话,我们在之前有讲过,就是因为,在MRC下,block对__block修饰的变量都是相当于弱引用,不会对引用计数器+1。