前言

本文作为《Objective-C 高级编程》读书笔记的第二篇,给大家带来的是关于 Blocks 的知识点总结。


概念

Blocks 是 C 语言的扩充功能,可以用一句话来表示 Blocks 的扩充功能:带有自动变量(局部变量)的匿名函数。(Blocks 是闭包在 OC 语言中的实现,并不是 iOS 独有的概念,在 C++、Java 等语言也有实现闭包,只是名称不同而已)


优势

  1. 可代替 Delegate 完成回调,而不需要像 Delegate 那样繁琐
  2. 在某些方面,可代替 selector(如 NSNotificationCenter 在 addObserver 的时候,可以使用 block,而不用单独定义方法)
  3. 延长对象的生命周期(Block 会自动持有对象)
  4. 提高代码的复用性和可读性
  5. 常用于:View 动画、GCD、网络异步请求

语法

关于 Blocks 的语法,看下面一张图就可以啦:


Blocks 的实现

Blocks 的数据结构

对应的结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};

struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

通过该图,我们可以知道,一个 Block 实例实际上由 6 部分构成:

  1. isa 指针: 所有对象都有该指针,用于实现对象相关的功能

  2. flags: 用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用;

  3. reserved: 保留变量;

  4. invoke: 函数指针,指向具体的 block 实现的函数调用地址;

  5. descriptor: 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针;

  6. variables: capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中;


Block 存储域

Objective-C 中的 Stack 和 Heap

首先所有的 Objective-C 对象都是分配在 Heap 的。 在 OC 最典型的内存分配与初始化就是这样的:

NSObject *obj = [[NSObject alloc] init];

一个对象在 alloc 的时候,就在 Heap 分配了内存空间。

Stack 对象通常有速度的优势,而且不会发生内存泄露问题。那么为什么 OC 的对象都是分配在 Heap 的呢? 原因在于:

  • Stack 对象的生命周期所导致的问题。例如一旦函数返回,则所在的 Stack Frame(栈帧)就会被销毁。那么此时返回的对象也会一并销毁。这个时候我们去 retain 这个对象是无效的。因为整个 Stack Frame 都已经被销毁了。简单而言,就是 Stack 对象的生命周期不适合 OC 的引用计数内存管理方法。

  • Stack 对象不够灵活,不具备足够的扩展性。创建时长度已经是固定的,而stack对象的拥有者也就是所在的 Stack Frame

Block 类型

应用程序的内存分配:

在 OC 中,一共有 3 种类型的 Block:

_NSConcreteGlobalBlock

_NSConcreteGlobalBlock:全局的静态 Block,不会访问任何外部变量。

_NSConcreteStackBlock

_NSConcreteStackBlock:保存在栈中的 Block,当函数返回时会被销毁。(ARC 中系统实现了自动 copy, 将创建在栈上的 Block 自动拷贝到堆上,所以不存在此类型的 Block)

_NSConcreteMallocBlock

_NSConcreteMallocBlock:保存在堆中的 Block,当引用计数为 0 时会被销毁。(即成为正常的 OC 对象)


Block 循环引用

如果在 Block 中使用附有 __strong 修饰符的对象类型自动变量,那么当 Block 从栈复制到堆时,该对象为 Block 所持有,于是便导致了循环引用的产生。

如图所示:self 持有 Block,Block 持有 self,这正是循环引用。

MRC

在 MRC 下,使用 __block 说明符来避免 Block 中的循环引用。

这是由于当 Block 从栈复制到堆时,若 Block 使用的变量为附有 block 说明符的 id 类型或对象类型的自动变量,不会被 retain;若 Block 使用的变量为没有 block 说明符的 id 类型或对象类型的自动变量,则被 retain;若 Block 使用的变量为没有 __block 说明符的 id 类型或对象类型的自动变量,则被 retain。

ARC

在 ARC 下,为了避免这种情况发生,可以在变量声明时用 weak 修饰符修饰变量 self,让 block 不强引用 self,从而破除循环。iOS4 和 Snow Leopard 由于对 weak 的支持不够完全,可以用 unsafe_unretained 代替。

使用 Block 成员变量避免循环引用:

比较

下面对使用 block 变量避免循环引用的方法和使用 weak 修饰符及 __unsafe_unretained 修饰符避免循环引用的方法做个比较。

使用 __block 变量的优点如下:

  • 通过 __block 变量可控制对象的持有期间
  • 在不能使用 __weak 修饰符的环境中不使用 __unsafe_unretained 修饰符即可(不必担心悬垂指针

    在执行 Block 时可动态地决定是否将 nil 或其他对象赋值在 __block 变量中。

使用 __block 变量的缺点如下:

  • 为避免循环引用必须执行 Block

    存在执行了 Block 语法,却不执行 Block 的路径时,无法避免循环引用。若由于 Block 引发了循环引用时,根据 Block 的用途选择使用 __block 变量、__weak 修饰符或 __unsafe_unretained 修饰符来避免循环引用。


要点

  1. Block 执行的代码其实在编译的时候就已经准备好了

  2. 本身 Block 就是一个普通的 OC 对象。正因为它是对象,Block 可以被作为参数传递,可以作为返回值从一个方法返回,可以用来给变量赋值

  3. __block 修饰符在 MRC 下不会进行引用计数加 1,而 ARC 下则会加 1

  4. 对于 Block 外的变量引用,Block 默认是将其复制到其数据结构中来实现访问的

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


参考