__block用结构体使用forwarding指针的原因

更新记录

时间 版本修改
2020年5月10日 初稿

1. 前言

  • 阅读本文需要先了解Block存储域的问题,即需要了解栈Block、堆Block、全局Block的三种分类。如Block存储域学习
  • 一言以蔽之,forwarding字段是在栈Block复制到堆Block时,改变指向,从而指向真实的,且唯一的(仅有一份,在堆上,敲黑板)存储实际变量的结构体(即类似__Block_byref_val_0的结构体)

2. Block从栈copy到堆上的细节

2.1 《Objective-C高级编程 iOS与OS X多线程和内存管理》的说到的几个知识点

2.2 学习以下涉及block从栈复制到堆上的源代码

2.2.1 原始代码如下:
#import <Foundation/Foundation.h>
#include <stdio.h>
int main(int argc, char * argv[]) {
    __block int val = 0;

    printf("&val:%p, val:%d \n", &val, val);
    
    void (^blk)(void) = ^{
        printf("inBlock    &val:%p, val:%d \n", &val, val);
        ++val;
    };
    
    printf("&val:%p, val:%d \n", &val, val);
    blk();
    printf("&val:%p, val:%d \n", &val, val);
    ++val;
    printf("&val:%p, val:%d \n", &val, val);
    return 0;
}
  • 输出结果为:
&val:0x7ffee15e9ca8, val:0 
&val:0x600002985e58, val:0 
inBlock    &val:0x600002985e58, val:0 
&val:0x600002985e58, val:1 
&val:0x600002985e58, val:2 
  • 由于在ARC环境下,使用strong修饰的变量指向block,会持有这个block。因此临时变量block会从栈复制到堆上,所以__block变量也会从栈上复制到堆上。
  • 观察代码输出的结果,val变量的地址前后是改变过的
2.2.2 使用clang得到block转换后的代码如下
struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

        printf("inBlock    &val:%p, val:%d \n", &(val->__forwarding->val), (val->__forwarding->val));
        ++(val->__forwarding->val);
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;__main_block_copy_0
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};

    printf("&val:%p, val:%d \n", &(val.__forwarding->val), (val.__forwarding->val));

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    printf("&val:%p, val:%d \n", &(val.__forwarding->val), (val.__forwarding->val));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    printf("&val:%p, val:%d \n", &(val.__forwarding->val), (val.__forwarding->val));
    ++(val.__forwarding->val);
    printf("&val:%p, val:%d \n", &(val.__forwarding->val), (val.__forwarding->val));
    return 0;
}
  • 按照之前所剖析过的block实现,val变量都是通过val.__forwarding->val的方式(即__forwarding指针的方式)访问的,所以我们实际打印出来的val变量的地址和值,其实就是val.__forwarding->val的地址和值。
  • 如这份代码所示,当我们将Block从栈拷贝到堆上时,Block所捕获的__block变量也会从栈拷贝到堆上,但是此时我们在该函数的作用域内(即Block外)仍然是可以对val变量进行修改的。
  • 为了将上述修改进行同步,在将__block变量从栈拷贝到堆上时,栈上的__Block_byref_val_0结构体的__forwarding指针将会指向堆上的__Block_byref_val_0结构体。所以此时,val变量(即val.__forwarding->val变量)的地址改变了。见下图:
  • 上述的拷贝动作,是由__main_block_copy_0实现的。

2.3 作为对比,查看下列源代码

  • 对比的代码
#import <Foundation/Foundation.h>
#include <stdio.h>
int main(int argc, char * argv[]) {
    int intVarInStack = 0;
    printf("&intVarInStack:%p, intVarInStack:%d \n", &intVarInStack, intVarInStack);

    __block int val = 0;
    
    printf("&b:%p, b:%d \n", &val, val);
    NSArray *blockArray = [[NSArray alloc] initWithObjects:^{
        printf("hello world!");},
        ^{
            printf("inBlock2    &val:%p, val:%d \n", &val, val);
            ++val;
        },
    nil];
    
    printf("&val:%p, val:%d \n", &val, val);
    ++val;
    return 0;
}
  • 输出结果为:
&intVarInStack:0x7ffeedfa1cac, intVarInStack:0 
&val:0x7ffeedfa1ca0, val:0 
&val:0x7ffeedfa1ca0, val:0 
&val:0x7ffeedfa1ca0, val:1 
  • 由于block作为函数参数传入,且不是GCD的API,也不是Cocoa框架中含有usingBlock的方法,因此是一个栈block。(其实,第一个block是一个全局block,因为它没有引用外部变量,不过即使引用了外部变量,第一个block也是一个堆block,读者可以自己尝试。亲测如此)
  • 由于并不涉及Block从栈到堆上的copy,所以val变量的地址也不需要改变,而是存在栈上。从它的地址和intVarInStack变量地址经凑在一起,也可以证明它确实是在栈上的变量。

3. 总结

  • __forwarding指针是为了在__block变量从栈复制到堆上后,在Block外对__block变量的修改也可以同步到堆上实际存储__block变量的结构体上。

参考

posted @ 2020-05-10 19:08  HelloWooo  阅读(1318)  评论(0编辑  收藏  举报