美文网首页
block的变量以及内存管理

block的变量以及内存管理

作者: 恬甜咖啡糖_0301 | 来源:发表于2018-01-17 15:31 被阅读0次

    有些疑问

    1.为什么在block里面改变获取的外部变量的值编译会报错?
    2.在block里面改变任何获取的外部变量的值都会报错吗?
    3.__block修饰的变量为什么可以改变值?

    分析 block 的__main_block_impl_0

    查看block_test_with_external_variable编译结果

    看看其中一段编译结果:

      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _external_variable, __Block_byref_b_external_variable_0 *_b_external_variable, int flags=0) : external_variable(_external_variable), b_external_variable(_b_external_variable->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
     static void __main_block_func_0(struct __main_block_impl_0 *__cself, int b_form_variable) {
      __Block_byref_b_external_variable_0 *b_external_variable = __cself->b_external_variable; // bound by ref
      int external_variable = __cself->external_variable; // bound by copy
    
                (b_external_variable->__forwarding->b_external_variable) += 1;
                b_form_variable += 1;
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_92_0j5cq12s6cx44km6rp1q00ww0000gn_T_main_888b43_mi_0,(b_external_variable->__forwarding->b_external_variable));
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_92_0j5cq12s6cx44km6rp1q00ww0000gn_T_main_888b43_mi_1,external_variable);
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_92_0j5cq12s6cx44km6rp1q00ww0000gn_T_main_888b43_mi_2,b_form_variable);
    
            }
    

    block如何获取外部局部变量

    至此可以看出获取外部变量的原理:
    block 通过__main_block_impl_0函数通过参数值传递获取到external_variable变量保存到 __main_block_impl_0 结构体的同名变量 external_variable
    通过以下代码 取出 external_variable,可以看出是通过复制该变量的值(代码编译到该处,捕获外部变量的瞬时值)到其数据结构中来实现访问的。

    int external_variable = __cself->external_variable; // bound by copy 
    

    构造函数__main_block_impl_0 冒号后的表达式 external_variable(_external_variable)的意思是,用 _external_variable 初始化结构体成员变量external_variable
    有四种情况下应该使用初始化表达式来初始化成员:
    1:初始化const成员
    2:初始化引用成员
    3:当调用基类的构造函数,而它拥有一组参数时
    4:当调用成员类的构造函数,而它拥有一组参数时
    参考:C++类成员冒号初始化以及构造函数内赋值

    __block修饰的变量

    block 通过__main_block_impl_0函数将变量b_external_variable的指针保存到 __main_block_impl_0结构体的同名变量 b_external_variable__forwarding
    通过以下代码 访问 b_external_variable,可以看出是通过传到该变量的指针地址实现访问并修改的

    b_external_variable->__forwarding->b_external_variable
    

    现在就可以回答开头的第一个和第三个问题了

    加了 __block修饰后,简单解释下几个概念

    __block修饰后代码量增加了一些代码

    struct __Block_byref_b_external_variable_0
    {
      void *__isa;//对象指针
    __Block_byref_b_external_variable_0 *__forwarding;//指向自己的指针
     int __flags;//标志位变量
     int __size;//结构体大小
     int b_external_variable;//外部变量
    };
    
    1. __Block_byref_b_external_variable_0 结构体:用于封装 __block 修饰的外部变量。
    2. __main_block_copy_0 函数:当 block 从栈拷贝到堆时,调用此函数。
    3. __main_block_dispose_0 函数:当 block 从堆内存释放时,调用此函数。
      源码中的 __block int b_external_variable 翻译后变成了 __Block_byref_b_external_variable_0 结构体指针变量 b_external_varible,通过指针传递到 block 内。但 __Block_byref_b_external_variable_0 结构体需要注意在已有结构体指针__isa指向 __Block_byref_b_external_variable_0 的同时,结构体里面还多了个__forwarding 指向自己的指针变量。

    内存管理

    已经说过 block 的三种类型 _NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock,它们在内存中的分布如下:

    block_memory segments.png
    _NSConcreteGlobalBlock

    当 block 写在全局作用域时,即为 NSConcreteGlobalBlock类型;此类型处于内存的 data area 段,此处没有局部变量的骚扰,运行不依赖上下文,可以通过指针安全访问,内存管理也简单的多。

    _NSConcreteStackBlock

    _NSConcreteStackBlock 类型的block处于内存的栈区。在内存栈区,如果其变量作用域结束(函数返回时),这个 block 就被废弃,block 上的 __block 变量也同样会被废弃。所以_NSConcreteStackBlock的block的作用有限, 为了解决这个问题,block 提供了 copy 的功能,将 block 和 __block 变量从栈拷贝到堆,也就转变为了_NSConcreteMallocBlock类型。

    _NSConcreteMallocBlock

    当 block 从栈拷贝到堆后,该block的__isa将写入_NSConcreteMallocBlock,变量生命周期将被影响,就算变量作用域结束,还可以继续使用 block

    此时,堆上的 block 类型为 _NSConcreteMallocBlock,所以会将 _NSConcreteMallocBlock 写入 isa

    前面我们已经发现 __Block_byref_b_external_variable_0 内部的成员变量都是通过访问 __forwarding 指针完成的。为了保证能正确访问栈上的 __block 变量,进行 copy 操作时,外部变量不会被复制,会将栈上的该变量的结构体里 __forwarding 指针指向了堆上的 block 结构体实例,因此该变量的引用计数器保持不变。

    为什么要将原本指向栈区的结构体的__forwarding指针,去指向堆区的结构体呢?
    想想刚开始为什么要给 block 添加 copy 的功能,就是因为 block 获取了局部变量,当要在其他地方(超出局部变量作用范围)使用这个 block 的时候,由于访问局部变量异常,导致程序崩溃。为了解决这个问题,就给 block 添加了 copy 功能。在将 block 拷贝到堆上的同时,也必须要存在__forwarding的指针并指向堆上结构体。这样在超出局部变量作用范围后还想要想使用 __block 变量,就通过 __forwarding 访问并改变堆上变量,就不会出现程序崩溃了

    ARC中block

    在 ARC 开启的情况下, 绝大部分情况下只会有 NSConcreteGlobalBlockNSConcreteMallocBlock 类型的 block。因为在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上

    block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法, 可以看看一下代码示例

    typedef int (^block_with_return_and_argument)(int);
    block_with_return_and_argument func(int rate)
    {
        return ^(int count){return rate * count;};
    };
    
    NSArray* getBlockArray(block_with_return_and_argument argument_block)
    {
        /**即使开启ARC,但是 argument_block的__isa还是存放在__NSStackBlock__**/
        return [[NSArray alloc] initWithObjects:argument_block,
            nil];
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int rate = 0.f;
            NSArray *blockArray = getBlockArray(^(int count){return rate * count;});
            ((block_with_return_and_argument)[blockArray lastObject])(3);
        }
    }
    

    如果想理解更多ARC和MRC不同环境下block的运行有哪些区别,可以点击这里,有一些我测试的小例子,加深理解


    还有最后一个问题没有回答“在block里面改变任何获取的外部变量的值都会报错吗?”

    经过上面的分析,我们知道在block里面想要访问、改变变量,关键就是在有效作用域拿到变量的有效地址即可。将block copy到堆,以及使用特殊符__block 修饰等相关处理也是为了解决这一问题。所以在block里面改变任何获取的外部变量(非__block修饰)的值不一定会报错,因为只有外部局部非静态变量的作用域是在相应函数中有效,在栈中管理,运行时分配内存,在block中修改才会报错。

    先来看看关于变量的几个概念

    按变量类型
    静态变量:编译时在静态存储区分配空间,分配在data段里的数据在编译时就获得存储空间了。

    非静态变量:除全局变量外,都在栈中管理,运行时分配内存。

    按作用域
    全局变量:编译时在静态存储区分配空间。在程序运行结束前都有效。
    局部变量:除静态局部变量外,都在栈中管理,运行时分配内存。在相应函数中有效。除静态局部变量,其他局部变量在函数结束随着相应栈帧消失而消失。

    关于变量的总结
    初始化的—全局变量(自动,静态)静态局部变量,这些分配在data段里的数据在编译时就获得存储空间了。
    未初始化的全局变量(自动,静态)静态局部变量编译时分配在bss段(占位符,bss段大小),编译器自动赋0。(编译器并不分配空间,只是记录数据所需空间大小),此段占用内存空间(执行时),不占用可执行文件空间即磁盘上空间。
    而局部变量(非静态)在运行时才在栈里分配空间。
    C/C++变量在内存中的位置以及初始化问题

    所以在block全局变量静态变量,不需要加入__block也可以在block中被修改。
    全局变量的作用域、生命周期都比block要大要长,所以在block内可以直接使用、修改。
    静态变量存储在data段bss段,只是局部的静态变量有一个作用域限制,但是生命周期还是比block要大要长,所以在方案上,block在结构体内部获取了静态变量的指针,所以也可以直接修改。

    补充两点说明加深理解
    1.由于外部的变量的作用域与生命周期问题,即使block通过指针拿到该变量的修改复制权限,其实也很危险,但变量作用域到头释放的时候,block内部的指针就是野指针;

    2.针对oc对象,__block其实可以看作一种提醒编译器优化的标识,加上的 后该OC对象会在block结构体内会生成一个__block的对象(含有isa的结构体,含有该对象,flag),并且可以使该__block对象在栈和堆上都可以得到(fowarding指针),所以__block修饰的外部变量,当copy到堆上的时候,外部变量不会被复制,而是直接赋值指针地址,因此该变量的引用计数器保持不变。

    相关文章

      网友评论

          本文标题:block的变量以及内存管理

          本文链接:https://www.haomeiwen.com/subject/ouhyoxtx.html