有些疑问
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;//外部变量
};
__Block_byref_b_external_variable_0
结构体:用于封装 __block 修饰的外部变量。__main_block_copy_0
函数:当 block 从栈拷贝到堆时,调用此函数。__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
,它们在内存中的分布如下:
_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 开启的情况下, 绝大部分情况下只会有 NSConcreteGlobalBlock
和 NSConcreteMallocBlock
类型的 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到堆上的时候,外部变量不会被复制,而是直接赋值指针地址,因此该变量的引用计数器保持不变。