iosiOS进阶iOS面试知识点收集

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

2016-05-11  本文已影响1066人  Jerry4me
Objective-C高级编程 iOS与OS X多线程和内存管理

Objective-C高级编程读书笔记三部曲已经写完, 另外两篇如下 :
Objective-C高级编程读书笔记之内存管理
Objective-C高级编程读书笔记之GCD


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结构体的成员作简单解释


2. block的类型

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

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

说明 :

全局块(_NSConcreteGlobalBlock)

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

栈块(_NSConcreteStackBlock)

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

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

堆块(_NSConcreteMallocBlock)

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

ARC下的block类型

因为ARC下默认变量修饰符为__strong, 所以我们接触到的block几乎全是堆block和全局block.
<pre>
ARC下, blk = block; 相当于 blk = [block copy];
</pre>


3. block特性

1. 截获自动变量值

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

拷贝

<pre>
int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
</pre>

输出为
age = 10

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

引用地址

<pre>
__block int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
</pre>

输出为
age = 18

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

2. 截获对象

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

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

4. __block修饰符

为什么__block修饰符修饰的变量就能够在block内部修改呢?? 原因在此
利用<pre> clang -rewrite-objc 源代码文件名 </pre>便可揭开其神秘的面纱.

<pre>
__block int val = 10;
转换成
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
</pre>

天哪! 一个局部变量加上__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进行copy操作.

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

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

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

<pre> blk = [[[[blk copy] copy] copy] copy]; </pre>

该源码可解释如下 :
<pre>
{
block tmp = [blk copy]; // block被tmp持有
blk = tmp; // block被tmp和blk持有
}
// tmp超出作用域, 其指向的block也被释放, block被blk持有
{
block tmp = [blk copy]; // block被tmp和blk持有
blk = tmp; // blk指向的旧block释放, 并强引用新block, 最终block被tmp和blk持有
}
// tmp超出作用域, 其指向的block也被释放, block被blk持有
...下面不断重复该过程
</pre>

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

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

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


6. block的循环引用

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

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


7. Block总结 :

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


欢迎大家关注@Jerry4me, 关注菜鸟成长_. 我会不定时更新一些学习心得与文章.

上一篇下一篇

猜你喜欢

热点阅读