block没那么难(二):block和变量的内存管理


本系列博文总结自《Pro Multithreading and Memory Management for iOS and OS X with ARC》


了解了 block的实现,我们接着来聊聊 block 和变量的内存管理。本文将介绍可写变量、block的内存段、__block变量的内存段等内容,看完本文会对 block 和变量的内存管理有更加清晰的认识。

上篇文章举了个例子,在 block 内获取了一个外部的局部变量,可以读取,但无法进行写入的修改操作。在 C 语言中有三种类型的变量,可在 block 内进行读写操作

  • 全局变量
  • 全局静态变量
  • 静态变量

全局变量全局静态变量 由于作用域在全局,所以在 block 内访问和读写这两类变量和普通函数没什么区别,而 静态变量 作用域在 block 之外,是怎么对它进行读写呢?通过 clang 工具,我们发现原来 静态变量 是通过指针传递,将变量传递到 block 内,所以可以修改变量值。而上篇文章中的外部变量是通过值传递,自然没法对获取到的外部变量进行修改。由此,可以给我们一个启示,当我们需要修改外部变量时,是不是也可以像 静态变量 这样通过指针来修改外部变量的值呢?

Apple 早就为我们准备了这么一个东西 —— “__block”

__block 说明符

按照惯例,重写一小段代码看看 __block 的真身

在加了 __block 之后,代码量增加了不少,仔细查看,其实只是比原来多了

OC源码中的 __block intValue 翻译后变成了 __Block_byref_intValue_0 结构体指针变量 intValue,通过指针传递到 block 内,这与前面说的 静态变量 的指针传递是一致的。除此之外,整体的执行流程与不加 __block 基本一致,不再赘述。但 __Block_byref_intValue_0 这个结构体需特别注意下

11

在已有结构体指针指向 __Block_byref_intValue_0 时,结构体里面还多了个 __forwarding 指向自己的指针变量,难道不显得多余吗?一点也不,本文后面会阐述。


block 的内存管理

在前文中,已经提到了 block 的三种类型 NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock,见名知意,可以看出三种 block 在内存中的分布

12

_NSConcreteGlobalBlock

除了上述描述的两种情况,其他形式创建的 block 均为 stack block

_NSConcreteGlobalBlock 类型的 block 处于内存的 ROData 段,此处没有局部变量的骚扰,运行不依赖上下文,内存管理也简单的多。

_NSConcreteStackBlock

_NSConcreteStackBlock 类型的 block 处于内存的栈区。global block 由于处在 data 段,可以通过指针安全访问,但 stack block 处在内存栈区,如果其变量作用域结束,这个 block 就被废弃,block 上的 __block 变量也同样会被废弃。

13

为了解决这个问题,block 提供了 copy 的功能,将 block 和 __block 变量从栈拷贝到堆,就是下面要说的 _NSConcreteMallocBlock

_NSConcreteMallocBlock

当 block 从栈拷贝到堆后,当栈上变量作用域结束时,仍然可以继续使用 block

14

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

  1. impl.isa = &_NSConcreteMallocBlock;

如果你细心的观察上面的转换后的代码,会发现访问结构体 __Block_byref_intValue_0 内部的成员变量都是通过访问 __forwarding 指针完成的。为了保证能正确访问栈上的 __block 变量,进行 copy 操作时,会将栈上的 __forwarding 指针指向了堆上的 block 结构体实例。


block 的自动拷贝和手动拷贝

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

block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法;

但方法/函数在内部已经实现了一份拷贝了 block 参数的代码,或者如果编译器自动拷贝,那么调用者就不需再手动拷贝,比如:

  • 当 block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 被赋值给 __strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy_Block_copy 进行拷贝;

让我们看个 block 自动拷贝的例子

上面的 block 获取了外部变量,所以是创建在栈上,当 func 函数返回给调用者时,脱离了局部变量 rate 的作用范围,如果调用者使用这个 block 就会出问题。那 ARC 开启的情况呢?运行这个 block 一切正常。和我们的预期结果不一样,ARC 到底给 block 施了什么魔法?我们将上面的代码翻译下

转换后出现两个新函数 objc_retainBlockobjc_autoreleaseReturnValue。如果你看过runtime 库(点此下载) ,在 runtime/objc-arr.mm 文件中就有这两个函数的实现:

通过上面的代码和注释,意思就很明显了,由于 block 字面量是创建在栈内存,通过 objc_retainBlock() 函数拷贝到堆内存,让 tmp 重新指向堆上的 block,然后将 tmp 所指的堆上的 block 作为一个 Objective-C 对象放入 autoreleasepool 里面,从而保证了返回后的 block 仍然可以正确执行。

看完了 block 的自动拷贝,那么看看在 ARC 下需要手动拷贝 block 的例子

一个例子就了然,返回的数组里面的 block 是不可用的,需要再手动拷贝一次才可以,这个较为简单,就不作过多解释。

关于 block 的拷贝操作可以用一张表总结下

16

block 拷贝的讲解就到此为止,有兴趣可以了解下 block 的多次拷贝。


__block 变量的内存管理

上面啰嗦一堆,这小节主要用图说话,必要时加文字说明。

  • 当 block 从栈内存被拷贝到堆内存时,__block 变量的变化如下图。需要说明的是,当栈上的 block 被拷贝到堆上,堆上的 block 再次被拷贝时,对 __block 变量已经没有影响了。

    17

    18

  • 当多个 block 获取同一个 __block 变量,block 从栈被拷贝到堆时

    19

  • 当 block 被废弃时,__block 变量被释放

    20

  • __forwarding
    前文已经说过,当 block 从栈被拷贝到堆时,__forwarding 指针变量也会指向堆区的结构体。但是为什么要这么做呢?为什么要让原本指向栈区的结构体的指针,去指向堆区的结构体呢?看起来匪夷所思,实则原因很简单,要从 __forwarding 产生的缘由说起。想想起初为什么要给 block 添加 copy 的功能,就是因为 block 获取了局部变量,当要在其他地方(超出局部变量作用范围)使用这个 block 的时候,由于访问局部变量异常,导致程序崩溃。为了解决这个问题,就给 block 添加了 copy 功能。在将 block 拷贝到堆上的同时,将 __forwarding 指针指向堆上结构体。后面如果要想使用 __block 变量,只要通过 __forwarding 访问堆上变量,就不会出现程序崩溃了。

一定有很多人会猜 1,其实打印 2。原因很简单,当栈上的 block 被拷贝到堆上时,栈上的 __forwarding 也会指向堆上的 __block 变量的结构体。

上面的代码中 ^{++val;}++val; 都会被转换成 ++(val.__forwarding->val);,堆上的 val 被加了两次,最后打印堆上的 val2

图解

21


block 和变量的内存管理终于讲完了,看似很长,只要了解本质,其实很简单。期待下篇文章《block没那么难(三):block和对象的内存管理》

1 4 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部