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变量的转码分析

  1. 编译器会将__block变量包装成一个对象
  2. __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

image.png

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 对象内存管理

  1. OC 测试代码


  2. 转C++后


block(指定都是堆block,应为只有堆block才会引用对象)内部访问的对象类型,会根据对应的强弱修饰符__strong/__weak,调用对应的函数_Block_object_assign进行内存处理

  • strong 修饰的,block 会强引用对象
  • weak 修饰,block 不会强引用对象
  • __block修饰,block 会强引用该变量对象

4.6 __block的内存管理总结

  1. 当block在栈上时,并不会对__block变量产生强引用

  2. 当block被copy到堆时

  • 会调用block内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会对__block变量形成强引用(retain)
  1. 当block从堆中移除时
  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的__block变量(release)

4.7 __block修饰的对象age 变量,仍然是存在于栈上,栈上变量,堆上的block,如何关联引用?

  • 实际上,当 block 被 copy 到堆上时,其访问的__block变量也会被 copy 到堆上,如下情况图解
  1. 将 block copy到堆



  2. 废弃 block



5 block内部访问 对象类型的auto变量__block变量的内存管理总结

  1. 当block在栈上时,对它们都不会产生强引用

  2. 当block拷贝到堆上时,都会通过copy函数来处理它们
    __block变量(假设变量名叫做a)

_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
  1. 对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
  1. 当block从堆上移除时,都会通过dispose函数来释放它们
    __block变量(假设变量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
  1. 对象类型的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 personperson之间的关系

7.4 总结:被__block修饰的对象类型内存管理

  1. __block变量在栈上时,不会对指向的对象产生强引用

  2. __block变量被copy到堆时

  • 会调用__block变量内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会根据所指向对象的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
  • 注意:这里仅限于ARC时会retain,MRC时不会retain
  1. 如果__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到堆时

  1. MRC 环境
  • 通过对栈 block 进行 copy 操作,转为堆 block,堆 block 内捕获的 __block person 结构体也会被 copy 到堆上,但是__block person 结构体不会对 person 对象进行强引用


Demo:BlockTest-__block-对象类型03

  1. ARC 环境
  • __strong


  • __weak


Demo:BlockTest-__block-对象类型04

前面测试用的代码,开始没想过要保存的,后面为了方便自己调试和截图就补上一点点 demo 代码了,不过没有也没关系,截图也已经非常清晰,自己敲一遍好了,理解更深刻。


文/Jacob_LJ(简书作者)
PS:如非特别说明,所有文章均为原创作品,著作权归作者所有,转载需联系作者获得授权,并注明出处,所有打赏均归本人所有!

推荐阅读更多精彩内容