Block捕获__block局部变量的底层原理

更新记录

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

1. 前言

上篇文章《Block中修改局部变量的值为什么必须声明为__block类型》中,考虑到篇幅不宜过长,并没有给出探索Block捕获__block局部变量的代码例子。本文准备较详细地探索Block捕获__block局部变量的底层原理,也作为上篇文章的补充说明

2. Block捕获__block局部变量代码剖析

2.1 Block捕获__block局部变量代码示例

  • Objective-C代码如下:
#include <stdio.h>
int main(int argc, char * argv[]) {
    __block int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        ++val;
        printf(fmt,val);
    };
    val = 2;
    fmt = "These value were changed. val = %d\n";
    blk();
    return 0;
}
  • 代码输出结果为:val = 3,可见val = 2的赋值是起作用的,但是fmt的赋值是不影响block内部的。

2.2 使用clang转换后的C++源代码剖析

  • 转换后的源代码如下:
struct __Block_byref_val_0 {
  void *__isa;      //isa指针,指向类对象;(可类比NSObject)
__Block_byref_val_0 *__forwarding;  //指向 __Block_byref_val_0 指针;有时候会指向自己;后续文章会详细介绍此成员变量的作用
 int __flags;   //暂时可忽略
 int __size;    //记录本结构体(即__Block_byref_val_0)的内存带下
 int val;       //捕获的局部变量的存储字段
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  __Block_byref_val_0 *val; // by ref; (本来只是一个int变量,捕获__block变量,就变成这么一个结构体了)
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, __Block_byref_val_0 *_val, int flags=0) : fmt(_fmt), 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
  const char *fmt = __cself->fmt; // bound by copy

        ++(val->__forwarding->val);
        printf(fmt,(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;
  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), 10};   //声明并初始化一个 __Block_byref_val_0 结构体,其中记录了捕获的成员变量的值 10
    const char *fmt = "val = %d\n";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, (__Block_byref_val_0 *)&val, 570425344));        //声明并初始化 __main_block_impl_0 结构体,其中保存了 val 变量的地址
    (val.__forwarding->val) = 2;    //注意,这里是通过__forwaring来访问的,才可以正确同步真正存储数值的变量上。(敲黑板)

    fmt = "These value were changed. val = %d\n";
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}
  • 转换后的源代码和捕获非__block变量的源代码的区别
    • 局部变量使用__Block_byref_val_0结构体来表示
    • __Block_byref_val_0结构体中含有isa指针,和OC对象一致
    • 含有__Block_byref_val_0 *__forwarding成员变量,指向相同的结构体
      • 至于为什么需要__forwarding这个成员变量,后续会单独写一篇文章说明。
    • __main_block_desc_0结构体中多了2个成员变量,都是函数指针,分别是__main_block_copy_0函数和__main_block_dispose_0函数
      • 这2个函数的作用,后续会单独写一篇文章说明。

3. 总结

  • 从转换之后的源代码可以看到:
    • 添加了__block修饰符的局部变量,变成了一个结构体。结构体中保存了实际的变量数值。
    • Block用结构体(即__main_block_impl_0)记录了包含实际存储变量(即int val)的结构体```__Block_byref_val_0````的地址
      • 所以可以在Block中修改该变量并同步到真实的变量上。
      • 所以在Block外部修改了局部变量(实质上是修改了__Block_byref_val_0结构体中的int val),Block内部也会受到影响
  • 为什么捕获__block局部变量不可以像《Block中修改局部变量的值为什么必须声明为__block类型》中提到的捕获局部静态变量的方式,直接捕获变量的地址?
    • 因为Block语法生成的Block上,可以存有超过其变量作用域的被截获对象(比如某个栈变量)。栈变量作用域结束时,该栈变量就被丢弃了。而如果Block仅仅只是记住了之前栈变量的内存地址,后续通过该地址访问到的值已经是一个随机值了。(也就是说,记录的指针已经变成了一个野指针。)
posted @ 2020-05-08 22:57  HelloWooo  阅读(434)  评论(0编辑  收藏  举报