• 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
    ";
        void (^blk)(void) = ^{
            ++val;
            printf(fmt,val);
        };
        val = 2;
        fmt = "These value were changed. val = %d
    ";
        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
    ";
        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
    ";
        
        ((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仅仅只是记住了之前栈变量的内存地址,后续通过该地址访问到的值已经是一个随机值了。(也就是说,记录的指针已经变成了一个野指针。)
  • 相关阅读:
    jQuery daily
    jQuery daily
    spring事务管理
    AspectJ AOP切面编程(XML配置)
    springAOP思想
    spring与web整合(交鸡肋,因为有前台框架封装了servlet)
    spring复杂对象注入四种方式
    spring的Bean注入和P标签使用
    spring Bean的作用域、生命周期和后处理器
    IoC容器实例化Bean三种方式
  • 原文地址:https://www.cnblogs.com/HelloGreen/p/12853627.html
Copyright © 2020-2023  润新知