block的变量以及内存管理

2018-01-17  本文已影响0人  恬甜咖啡糖_0301

有些疑问

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到堆上的时候,外部变量不会被复制,而是直接赋值指针地址,因此该变量的引用计数器保持不变。

上一篇下一篇

猜你喜欢

热点阅读