• Block中修改局部变量的值为什么必须声明为__block类型


    更新记录

    时间 版本修改
    2020年4月12日 初稿
    2020年5月7日 纠正错误:其实在使用__block变量的时候,实际的源代码变得复杂更多。考虑到篇幅和结构问题,本文后续只采用了Block捕获静态局部变量的例子,来查看Block捕获静态局部变量的实现。
    2020年5月8日 使用小标题序号,提升可读性。添加了关于char指针重新赋值的细节描述。

    1. 前言

    最近在重新且仔细地阅读《Objective-C 高级编程 iOS与OS X多线程和内存管理》,在阅读到 2.2 Blocks模式 这章时,看到Block中截获自动变量,对其进行重新赋值,会报“缺失__block修饰符”的编译错误。这引起了我的一些思考,在此叙述一下我的思考。

    2. 思考

    2.1 举书上的一个例子

    2.1.1 block中使用该对象
    id array = [[NSMutableArray alloc] init];
    void (^blk)(void) = ^{
      id obj = [[NSObject alloc] init];  
      [array addObject:obj];
    };
    
    • 上述代码是没有任何问题的
    2.1.2 block中对对象进行重新赋值
    id array = [[NSMutableArray alloc] init];
    void (^blk)(void) = ^{
        array = [[NSMutableArray alloc] init];
    };
    
    • 编译报错:Variable is not assignable (missing__block type specifier)
    • 网上很多参考资料上都说,给该变量加上__block修饰符就可以解决问题了。但是都没有谈到这个问题的深入之处

    2.2 Block捕获变量代码示例说明

    2.2.1 block不修改局部变量
    • block的使用代码:
    int main(int argc, const char * argv[]) {
        int val = 10;
        const char *fmt = "val = %d
    ";
        void (^blk)(void) = ^{
            printf(fmt,val);
        };
        val = 2;
        fmt = "These value were changed. val = %d
    ";
        blk();
        return 0;
    }
    
    • 输出结果为: val = 10
    • 转换之后的代码及对应的运行结果,很好理解:
      • 捕获了val这个局部变量,用以输出(Blocks的实质可参考我之前写的Blocks的实质学习总结
      • 也符合日常学习的认知:block捕获的非__block局部变量不受外部的改变
      • char* 类型的指针,再重新赋值时,指针变量会重新指向一片新的内存。而原来指针变量指向的内存并不受任何影响,仍然保持之前的值。所以该代码的输出结果是"val = %d",而不是"These value were changed. val = %d"。
    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {//最终的函数指针调用
      const char *fmt = __cself->fmt; // bound by copy
      int val = __cself->val; // bound by copy
      printf(fmt,val);
    }
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      const char *fmt;
      int val;              //block捕获的变量 val
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    int main(int argc, const char * argv[]) {
        int dmy = 256;
        int val = 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, val));//结构体带着参数val初始化并赋值
        val = 2;
        fmt = "These value were changed. val = %d
    ";
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);//函数指针调用
        return 0;
    }
    
    2.2.2 block捕获静态局部变量并修改
    • block的使用代码
    int main(int argc, char * argv[]) {
        static 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
    • 转换之后,代码和之前大致一样,但是有唯一的、细微的差别。
      • block用结构体__main_block_impl_0捕获的是val变量的地址(传地址,而非传值)
    • 就是这个细微的差别,可以做到使后续修改了变量val的值,block调用时也使用了更新之后的值,这是因为记录了val变量的地址(即静态存储区中),用地址访问当然是获取到最新的值。
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *val;   //block捕获的变量 val,注意,这里捕获的是指针!!!
      const char *fmt;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_val, const char *_fmt, int flags=0) : val(_val), fmt(_fmt) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) { //最终的函数指针调用
      int *val = __cself->val; // bound by copy
      const char *fmt = __cself->fmt; // bound by copy
      //这样就可以实现,在block中改变静态局部变量的值,是使用指针访问的
      ++(*val);
      printf(fmt,(*val));
    }
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    int main(int argc, char * argv[]) {
        static int val = 10;
        const char *fmt = "val = %d
    ";
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &val, fmt));  //结构体传递参数为val变量的地址!!!
        val = 2;
        fmt = "These value were changed. val = %d
    ";
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);  //函数指针调用
        return 0;
    }
    
    2.2.3 代码总结
    • 对于普通的auto局部变量(栈变量),Block捕获时,将值拷贝进Block用结构体的成员变量中。因此后续对局部变量的改变就再也影响不了Block内部。
    • 对于__block修饰的局部变量,Block捕获时,记录了该变量的地址。所以后续该变量的值改变了,block调用时,通过地址获取到的值仍然是最新的值。
    • 说明
      • 考虑到篇幅,没有介绍Block捕获__block局部变量的转换后的C++源代码。但是其本质和捕获局部静态变量是一致的,都是在Block用结构体中记录下了该变量的地址。
      • Block捕获__block局部变量的值的转换后C++代码会比,上述捕获静态局部变量的代码复杂很多。在后续的文章《Block捕获__block局部变量的底层原理》中有介绍Block捕获__block局部变量的底层原理。

    2.3 底层思考

    • 参考《Objective-C 高级编程 iOS与OS X多线程和内存管理》后续章节对Blocks的实现,我们可以知道,Blocks生成的结构体会捕获所用到的变量。
    • 内存指示图
    • 对于局部变量,Blocks默认捕获的是这个局部变量的值(即图中的MemoryObj变量), 可以通过对MemroyObj这个地址上的内容进行修改(本质是运用了C语言的*运算符)
    • 而添加了__block说明符,则Blocks捕获的是这个局部变量的内存地址,即Memroy值(C语言中使用&操作取得一个变量的地址),这样Blocks在内部就可以通过对Memory上的数据对修改(*memroy = xxx),且可以影响到Blocks外部。
    • 没有用__block修饰的局部变量,在Blocks内部捕获了,即使修改了也没有任何意义(外部不受影响),所以编译器当初就设计了这个编译报错,避免产生不可预知的bug。
    • 鉴于篇幅和结构,这里没有介绍Block捕获__block修饰的变量的C++代码情况,关于该知识,可参考下一篇文章《Block捕获__block局部变量的底层原理》
  • 相关阅读:
    windows 1061
    Golang 编程思维和工程实战
    Apache Tomcat jar Catalina
    MySQL Client/Server Protocol mysql协议
    蚂蚁集团万级规模 k8s 集群 etcd 高可用建设之路
    实习生系列之找实习的途径
    Yahoo!网站性能最佳体验的34条黄金守则
    onselectstart="return false"无法复制文字
    VS2008开发环境中容易遇到的3个问题之解决办法
    实践与交流:“三保险”为世界顶级安全防范软件ESET Nod32 4.0的正常使用“保驾护航”
  • 原文地址:https://www.cnblogs.com/HelloGreen/p/12684033.html
Copyright © 2020-2023  润新知