Objective-C高级编程读书笔记之blocks

503 查看

Objective-C高级编程
iOS与OS X多线程和内存管理

Blocks

这里有五道关于block的测试题, 大家可以去做做测试看看自己对block了解多少.

目录

  1. Block的定义
  2. Block有哪几种类型
  3. Block特性
  4. __block修饰符
  5. block调用copy方法的内部实现
  6. block的循环引用问题
  7. 总结

1. block的定义

block是Objective-C对于闭包的实现(闭包是一个函数<或者指向函数的指针>加上函数有关的自由变量).

block的数据结构

block也是对象, 以下对block结构体的成员作简单解释

  • isa : 指向Class对象的指针, 所有对象都有该指针
  • flags : 用于按 bit 位表示一些 block 的附加信息
  • reserved : 保留变量
  • invoke : 指向函数的指针, 指向block的实现代码
  • descriptor : 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
  • 捕捉到的变量 : 捕捉过来的变量,block 之所以能够访问它外部的局部变量,就是因为将这些变量(或对象的地址)拷贝到了结构体中。
  • copy : 用于保留捕获的对象
  • dispose : 用于释放捕获的对象

2. block的类型

  • 全局块(_NSConcreteGlobalBlock)
  • 栈块(_NSConcreteStackBlock)
  • 堆块(_NSConcreteMallocBlock)

这三种block各自的存储域如下表

设置对象的存储域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 程序的数据区域(.data区)
_NSConcreteMallocBlock

说明 :

  • 全局块存在于全局内存中, 相当于单例.
  • 栈块存在于栈内存中, 超出其作用域则马上被销毁
  • 堆块存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存

全局块(_NSConcreteGlobalBlock)

  • block定义在全局变量的地方
  • block没有截获任何自动变量

以上两个情况满足任意一个则该block为全局块, 全局块的生命周期贯穿整个程序, 相当于单例.

栈块(_NSConcreteStackBlock)

只要不是全局块, 且block没有被copy, 就是栈块.栈块的生命周期很短, 当前作用域结束, 该block就被废弃. 要想在当前作用域以外的地方使用该block, 应该把该block从栈copy到堆上

从栈复制到堆上的Block与__block变量

堆块(_NSConcreteMallocBlock)

简单来说, 栈块copy之后就变成堆块, 这简单吧~

ARC下的block类型

因为ARC下默认变量修饰符为__strong, 所以我们接触到的block几乎全是堆block和全局block.


3. block特性

1. 截获自动变量值

1> 对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的. 也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大.

拷贝

输出为
age = 10

2> 对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的.

引用地址

输出为
age = 18

意味着对于第一种情况, 在block外部修改变量的值并不会应该block内部变量的值.而第二种情况则反之.
并且第一种情况block内部不允许修改变量的值, 第二种情况下可以. (有例外, 静态变量, 静态全局变量, 全局变量即使不使用__block修饰符也可以在block内部修改其值)

2. 截获对象

对象不同于自动变量, 就算对象不加上__block修饰符, 在block内部能够修改对象的属性.
block截获对象与截获自动变量有所不同.
堆块会持有对象, 而不会持有__block修饰的对象, 而栈块永远不会持有对象, 为什么呢?

  1. 堆块作用域不同于栈块, 堆块可以超出其作用域地方使用, 所以堆块结构体内部会保留对象的强指针, 保证堆块在生命周期结束之前都能访问对象. 而对于__block对象为什么不会持有呢? 原因很简单, 因为__block对象会跟随block被复制到堆中, block再去引用堆中的__对象(后面会讲这个过程)..
  2. 栈块只能在当前作用域下使用, 所以其内部不会持有对象. 因为不存在在作用域之外访问对象的可能(栈离开当前作用域立马被销毁)

4. __block修饰符

为什么__block修饰符修饰的变量就能够在block内部修改呢?? 原因在此
利用

便可揭开其神秘的面纱.

天哪! 一个局部变量加上__block修饰符后竟然跟block一样变成了一个__Block_byref_val_0结构体类型的自动变量实例.

此时我们在block内部访问val变量则需要通过一个叫__forwarding的成员变量来间接访问val变量(下面会对__forwarding进行详解)


5. copy

block的copy操作究竟做了什么呢?

这里不得不提及__block变量的存储域

__block变量的配置存储域 block从栈复制到堆时的影响
从栈复制到堆并被Block持有
被Block持有

Block中使用__block变量

由上图可知, 对一个栈块进行copy操作会连同block与__block变量(不管有没有使用)在内一同copy到堆上, 并且block会持有__block变量(使用).
ps : 堆上的block及__block变量均为对象, 都有各自的引用计数

当然, 当block被销毁时, block持有的__block也会被释放

Block废弃和__block变量的释放

到这里我们能知道, 此思考方式与Objective-C的引用计数内存管理完全相同.

那么有人就会问了, 既然__block变量也被复制到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?? __forwarding 终于要闪亮登场了

复制__block变量

通过__forwarding, 无论实在block中, block外访问__block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量.


什么时候我们需要手动对block调用copy方法

前面我们说到 : 要想在当前作用域以外的地方使用该block, 应该把该block从栈copy到堆上. 实际上, 在ARC下, 以下几种情况下, 编译器会帮我们把栈上的block复制到堆中

  • block作为函数返回值返回时
  • 将block赋值给__strong修饰符id类型或block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递block时

理论上我们只有把block作为函数/方法的参数传入时才需要对block进行copy操作.

我们对不同地方的block调用copy会产生什么效果呢?

Block的类 副本源的配置存储域 拷贝效果
_NSConcreteStackBlock 从栈拷贝到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteMallocBlock 引用计数增加

所以, 不管block是什么类型, 在什么地方, 用copy方法都不会引起任何问题.如下表格所示. 就算是反复多次调用copy方法, 如

该源码可解释如下 :

我们知道, 这只是一个循环的过程, block被tmp持有 -> block被tmp和blk持有 -> block被blk持有 -> block被tmp和blk持有 -> ……

由此可得知, 在ARC下该代码也没有任何问题.

总结 : 如果block需要给作用域外的地方使用, 但是你不知道需不需要copy, 那就copy吧. 反正不会错


6. block的循环引用

这部分相信大家都清楚怎样做能破环, 所以我在这就只简单说两句

  • MRC下用__block可以避免循环引用(原因见上面block特性之截获自动变量值)
  • ARC下用__weak来避免循环引用

这里需要提醒大家的是, 只有堆块(_NSConcreteMallocBlock)才可能会造成循环引用, 其他两种block不会


7. Block总结 :

  • block相比函数更加方便, 高效, 苹果强烈推荐使用
  • ARC下编译器会帮助我们更好地管理block的生命周期
  • ARC下block属性声明为strong或copy其实都一样, 因为编译器内部会帮我们实现copy方法
  • 善用__weak(ARC)或__block(MRC)来避免循环引用

推荐几篇有关block的文章
谈Objective-C block的实现
让我们来深入简出block吧


附上另外两篇文章的链接
Objective-C高级编程读书笔记之内存管理
Objective-C高级编程读书笔记之GCD