block的变量以及内存管理

有些疑问

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342