Block底层实现分析02-__block使用
注:分析参考 MJ底层原理班 内容,本着自己学习原则记录
本文使用的源码为objc4-723
转 C++ 使用的命令 :
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
1 block 内部不能修改基本数据类型 auto 变量分析
1.1 转码后从函数作用域和值捕获逻辑分析
1.2 static 的基本数据变量可以在 block 内修饰分析
1.3 全局变量当然能在 block 内被修改
-
因为全局变量的全局特性:全局访问,所以可以随处修改
2 __block
修饰 auto 变量
2.1 一般情况我们都不想改变基本数据类型的 auto 变量类型,但还是希望能够在 block 内部修改 auto 变量的值
- 也就是说不想使用
static
修饰(这样会改变变量内存位置,static修饰时会使变量从栈区变成数据区,变量的声明周期被无限延长) - 也不会将基本数据类型的 auto 变量类型 转为 全局变量类型
2.2 使用__block
修饰 auto 变量
-
可以让auto变量在 block 内被修改
-
__block不能修饰静态变量(static)
-
__block不能修饰全局变量、静态变量(static)
2.3 __block
修改 auto变量
的转码分析
- 编译器会将
__block变量
包装成一个对象
- __block变量的 age 对象内存分析
源码对象 | 内存结构 | 备注 |
---|---|---|
3 __block
修饰的 age 及 block 外面访问的 age 对比
3.1 这些 age 都是同一个 age 吗?
3.2 通过打印和源码分析
3.2.1 从打印上看很明显,后两个(2、3) age 地址相同;
3.2.2 从源码上看,2和3 的 age 访问方式都是age.__forwarding->age
,被
__block
修饰后的 age 它是__Block_byref_age_0
类型结构体
3.2.3 从源码上看,第1个打印 age 地址并不是我们期待的,因为打印出来的这个 age 地址是__Block_byref_age_0
类型结构体内int age
成员变量的地址值
3.3 通过内存地址叠加分析
3.3.1 执行__block int age = 10;
代码后,age 变量即变成__Block_byref_age_0
类型的结构体,那么此时 age 的地址就是该结构体第一个成员变量的地址,age 结构体的地址 等于 __isa
指向的值
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
3.3.2 而后续的(2、3)访问的 age,都是通过age.__forwarding->age
方式访问,访问的 age 实际是结构体内部的 int age
变量,那么 age成员变量地址应该是结构体起始地址加上age 成员变量前的其他成员量占的内存字节数之和
3.3.3 通过底层转换形式,再次打印 age 结构体地址
通过上图方式,我们已经能够正确地获取到 age 结构体的地址,那么验证一下,age 成员变量
地址,是不是等于 age 结构体
地址加上age 成员变量
之前的其他变量所占内存字节之和呢?(答案当然是肯定啦)即:0x100604600 + (8+8+4+4) = 0x100604618
4 __block
的内存管理
我们可以从Block底层实现分析01的第6点知道,当 block 内部访问的变量是对象类型或被__block
修饰的基本变量类型时,block 的结构体中 Desc 结构体内会多出两个用于处理对象引用问题的函数成员变量,如下:
重点区别在
struct __main_block_desc_0
的结构体成员上
4.1 访问基本 auto 变量
block 转 C++后源码
4.2 访问对象类型的 auto 变量
的 block 转 C++后源码
访问对象类型的 auto 变量
的struct __main_block_desc_0
的结构体成员比访问`基本 auto 变量的多了两个成员
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
4.3 内存管理函数的实现
它们的作用就是用来处理对象类型的 auto 变量引用计数问题
4.4 访问__block修饰的 auto 变量
的 block 转 C++后源码
4.5 分别对比block 访问__weak对象
、__block对象
、默认 strong 对象
内存管理
-
OC 测试代码
-
转C++后
block(指定都是堆block,应为只有堆block才会引用对象)内部访问的对象类型,会根据对应的强弱修饰符
__strong/__weak
,调用对应的函数_Block_object_assign
进行内存处理
- strong 修饰的,block 会强引用对象
- weak 修饰,block 不会强引用对象
- __block修饰,block 会强引用该变量对象
4.6 __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)
4.7 __block
修饰的对象age 变量,仍然是存在于栈上,栈上变量,堆上的block,如何关联引用?
- 实际上,当 block 被 copy 到堆上时,其访问的
__block
变量也会被 copy 到堆上,如下情况图解
-
将 block copy到堆
-
废弃 block
5 block内部访问 对象类型的auto变量
、__block变量
的内存管理总结
-
当block在栈上时,对它们都不会产生强引用
-
当block拷贝到堆上时,都会通过copy函数来处理它们
__block
变量(假设变量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
- 对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
- 当block从堆上移除时,都会通过dispose函数来释放它们
__block
变量(假设变量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
- 对象类型的auto变量(假设变量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
6 __block
的__forwarding
指针
下述访问 age 的情况:
不知道你会不会奇怪,为什么访问的 age 是通过
age->__forwarding->age
方式访问?为什么要绕一大个弯来获取呢?其实这个与变量的栈堆拷贝有关的,从第4.7点中我们知道,
__block
修饰的变量会随 block 的栈堆位置变化而相应地发生变化,如下图示:
-
将 block copy到堆
那在__block
变量被拷贝时候,__forwarding
指针也会相应发生变化
- 证据1:同样的
age->__forwarding->age
访问方式,1的情况是,__block
变量age结构体 还在栈
空间,所以内部的 age 成员变量地址是栈地址样式(比较长😆),而2和3时候,__block
变量 age 已经 copy 到堆
控件,所以对应的 age 成员变量地址变为堆地址样式(比较短😆)
你同样可以打印一个局部
int height
变量地址,与其他打印的地址进行对比,同在堆或栈的变量或对象的地址不会相差太远(后续有机会,会详细分析堆栈内存结构相关知识)
-
证据2,在 MRC 环境下,下面图中代码足以证明 age 结构体的forwarding 指针会如前面
__forwarding
变化图那样,在 age结构体被堆栈block 捕获后,其值会发生对应的变化
7 被__block
修饰的对象类型
7.1 被__block
修饰的基本数据类型变量,在底层会将基本变量包装成成一个结构体对象
7.2 那被__block
修饰的对象类型呢?
7.3 对象类型会被包装成 __Block_byref_person_0
类型 block,block
与__block person
与 person
之间的关系
7.4 总结:被__block
修饰的对象类型内存管理
-
当
__block
变量在栈上时,不会对指向的对象产生强引用 -
当
__block
变量被copy到堆时
- 会调用
__block
变量内部的copy
函数 -
copy
函数内部会调用_Block_object_assign
函数 -
_Block_object_assign
函数会根据所指向对象的修饰符(__strong
、__weak
、__unsafe_unretained
)做出相应的操作,形成强引用(retain
)或者弱引用 - 注意:这里仅限于ARC时会retain,MRC时不会retain
- 如果
__block
变量从堆上移除
- 会调用
__block
变量内部的dispose
函数 -
dispose
函数内部会调用_Block_object_dispose
函数
_Block_object_dispose
函数会自动释放指向的对象(release
)
8 验证7.4总结
8.1 首先将环境调至 MRC,执行下述代码,理解清楚:栈 block
,堆 block
,栈的__block对象
,和堆的__block对象
的获取,可以参考上述第6点去理解
这里就不再用文字详细叙述了(用文字分析思考及逻辑变化过程需要太多篇幅了😝,如有不懂,欢迎微信QQ讨论,底部评论不会及时回复,而且评论区不能贴图解析很麻烦)
Demo:BlockTest-__block-对象类型01
8.2 验证:当__block
变量在栈上时,不会对指向的对象产生强引用
-
MRC 环境
当 Person 第一次执行完 release 操作时,即销毁,证明 __block
person 结构体并没有强引用 person。
Demo:BlockTest-__block-对象类型02
8.3 验证:当__block
变量被copy到堆时
- MRC 环境
- 通过对栈 block 进行 copy 操作,转为堆 block,堆 block 内捕获的
__block
person 结构体也会被 copy 到堆上,但是__block
person 结构体不会对 person 对象进行强引用
Demo:BlockTest-__block-对象类型03
- ARC 环境
-
__strong
-
__weak
前面测试用的代码,开始没想过要保存的,后面为了方便自己调试和截图就补上一点点 demo 代码了,不过没有也没关系,截图也已经非常清晰,自己敲一遍好了,理解更深刻。
文/Jacob_LJ(简书作者)
PS:如非特别说明,所有文章均为原创作品,著作权归作者所有,转载需联系作者获得授权,并注明出处,所有打赏均归本人所有!