从内存角度分析__block变量的访问过程

361 阅读3分钟

我们都知道:

  • __block不能修饰全局变量, 静态变量

  • block内部修改外部auto变量的值需要__block修饰这个变量, block在被拷贝到堆上的同时也会使 __block变量拷贝到堆上, __block变量会被包装成一个结构体, 通过__forwarding指针访问结构体的val的值, 此时栈区的结构体内部的__forwarding指针会指向堆区的结构体

WeChat1a57d58e424b9f794d37b636a8a268ce.png

  • 通过__forwarding指针可以实现无论是在block内部还是外部使用__block变量, 即__block变量配置在栈上还是堆上, 都可以顺利地访问同一个__block变量

从内存角度分析__block变量的访问过程

第一次断点打在没有堆区block引用__block变量的地方, a和c两个变量是为了更方便的在内存中查看__block变量的布局

WeChate8a2a25f9349a1ebed2fcb388de93f0d.png

看地址很难想象这三个变量在内存中是怎么布局的, 如果b没有__block修饰那么这三个变量的地址会是差值为4字节的连续的三个地址

CA2BAA88-18B7-4090-B4D2-87559257638C.png Clang rewrite后的代码

struct __Block_byref_b_0 {
  void *__isa;
__Block_byref_b_0 *__forwarding;
 int __flags;
 int __size;
 int b; // val
};
int a = 1;
__attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 2};
int c = 3;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__r01cw96d4wx2ll1z02jck5g00000gn_T_main_648eef_mi_0, &a);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__r01cw96d4wx2ll1z02jck5g00000gn_T_main_648eef_mi_1, &(b.__forwarding->b));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__r01cw96d4wx2ll1z02jck5g00000gn_T_main_648eef_mi_2, &c);

__block变量被包装成一个结构体, 根据结构体字节对齐共占据32个字节

可以看到b的获取方式&(b.__forwarding->b), 是通过__forwarding指针指向的结构体取成员val的值

看一下View Memory的内存布局:

B5A4A3D1-D7FC-4FE7-B32A-D0A1055B323F.png

栈区是从高地址向低地址连续分布, 小端低字节存低位

红色部分是变量a的1和变量c的3各4个字节, 还有因为字节对齐而空出的4个字节, 结构体成员最多占8个字节

中间未选中的部分就是变量b的内存布局:

struct __Block_byref_b_0 {
  void *__isa; // 0
__Block_byref_b_0 *__forwarding; // 0x7FFEEA44DC60 即b在栈区开始的地址
 int __flags;
 int __size; // 16进制的20 = 10进制的32 即32个字节 这个结构体字节对齐后的size
 int b; // val = 2
};

这里__forwarding指针为0x7FFEEA44DC60指向栈区的自己

计算可知打印的b的地址0x7ffeea44dc78为结构体中val的地址

过断点让堆区的block持有__block变量:

51CC6386-DA71-432F-9537-7C94992BC2A2.png

打印可知b的地址变为堆区的地址

再来看下View Memory的内存布局:

栈区

6FDCC732-61FF-448C-B0ED-7D1CCFC577EA.png

发现__forwarding指针变为0x6000032601E0, 指向堆区的拷贝

堆区

6C35F2B9-41D8-41CB-A2A8-EA773CAB6E82.png

这里前32个字节就是__block变量在堆区上的布局

struct __Block_byref_b_0 {
  void *__isa; // 0
__Block_byref_b_0 *__forwarding; // 0x6000032601E0 即b在堆区开始的地址
 int __flags;
 int __size; // 16进制的20 = 10进制的32 即32个字节 这个结构体字节对齐后的size
 int b; // val = 2
};

这里__forwarding指针为0x6000032601E0指向堆区的自己

计算可知此时打印的b的地址0x6000032601f8 为结构体中val的地址

所以, 只要__block变量被拷贝到了堆区, 不管是在block内部还是外部, 访问的都是堆上的对象

至此就从内存角度分析了__block变量的内存布局, 访问__block变量的过程原理, 打印地址的真实意义