如何更好地避免 block 循环引用

什么是循环引用

先说循环引用,这个东西大概是大多数程序猿都应该知道的常识,不仅是 Objective-C ,很多其他语言也有这个概念。假设有两个类 A 与 B,A 中存在一个 ivar objB,B 中存在一个 ivar objA。现在各有一个 A 与 B 类型的对象: a 与 b,如果:

1
2
a.objB = b;
b.objA = a;

这样就会产生循环引用,如下图:
image


iOS 中 ARC 内存管理机制用一句话概括起来就是:

如果一个对象对象没有被其他对象强引用,那么它就将被销毁


而由于 a 与 b 彼此强引用了对方,他们两者都无法被释放。要解决这个问题也简单,打破某一条强引用,将它变成弱引用就是了,比如可以:

1
2
3
4
5
6
@interface A

//@property (nonatomic, strong) B b; //原来的代码
@property (nonatomic, weak) B b; //将 strong 引用改成 weak 引用

@end

image

弱引用:weak类型的指针也可以指向对象,但是并不会持有该对象。并且在对象被释放的时候,自动被设置为 nil


那么 block 的循环引用也跟这个类似,只不过是其中一方变成一个 block:


image


block 循环引用什么时候会发生?举一个简单的栗子,有一个 BBBViewController 类型的对象 bCon,通过一个 navigation controller 来加载显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface BBBViewController : UIViewController

@property (nonatomic, strong) UILabel *label;
@property (strong, nonatomic) VoidBlock block;

@end

@implementation BBBViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.block = ^() {
self.label.text = @"circular reference";
};
}

@end

在这种情况下,当 bCon 从 navigationCon 中被 pop 掉时,bCon 并不会被释放(可通过在 dealloc 中的断点来观察)。简单一点的情况编译器还能检查出来,然后给你一个警告:


image


要是引用层级深一点,编译器可就不知道了:


image


要知道 block 为什么会引用了 self,就要稍微了解一下:

Objective-C block的实现

关于 block 的实现,还有对外部变量的引用方式,在文末的参考文章可以找到非常详尽的说明,大概来说就是:

block 实例会保存引用自外部的变量。可能是变量的值,也有可能是变量的指针(比如使用了 __block 修饰的变量)


那么,对于self呢。参考文献中的例子都没有涉及到这一点。而我们实际上经常使用 block 作为一些对象的回调,其中往往涉及到当前类的属性设置。下面看一个简单的例子:

1
2
3
4
5
6
//.h
@interface CCCObject : NSObject

@property (nonatomic) int value;

@end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//.m
typedef void (^VoidBlock)();

@implementation CCCObject

- (id)init
{
if (self = [super init]) {
VoidBlock block = ^() {
int i = 1024;
};
block();
}

return self;
}

@end

在 CCCObject 的 init 函数中定义了一个 block,这个 block 声明并初始化了一个局部变量,用 clang 将它翻译成 C++ 代码:

1
clang -rewrite-objc CCCObject.m

我们可以得到一个拥有 10W+ 行代码的文件,摘取主要部分如下:


image


CCCObjectinit_block_impl_0
这是 init 函数中 block 的结构定义。其中 impl 是每个 block 结构都会拥有的变量,它包含一些基本的信息:

  • isa 指针,所有对象都会拥有该指针,用来实现对象相关的功能;
  • flags,用来按 bit 位表示一些 block 的附加信息,在 block 进行 copy 的时候会使用到;
  • funcPtr,函数指针,指向 block 中实际实现的函数调用地址,例如本例中的 __CCCObject__init_block_func_0
  • desc,block 的附加描述信息,例如 size,还有 copy 跟 dispose 函数的指针


CCCObjectinit_block_func_0
从这个函数里面的那句代码就可以知道,这就是 block 在实际执行的时候调用的业务函数。


接下来对 block 做一些修改,让它修改 属性 value 的值:

1
2
3
4
5
6
7
8
9
10
11
- (id)init
{
if (self = [super init]) {
VoidBlock block = ^() {
self.value = 1024;
};
block();
}

return self;
}

接着还是 clang 一下,摘取主要代码部分如下:


image


留意 block 结构体的实现,里面多了一个成员变量 CCCObject *self,用来保存所引用的 CCCObject 对象,在实现函数中通过 objc_msgSend 函数来对 value 属性进行设置。换句话说,block 强引用了 self.


有兴趣的朋友可以再看看 _ICCCObject_init 这个函数,看起来好像一团糟的样子,因为它糅杂了 C 语言里面的两个最强利器:强制类型转换 以及 指针,而且还用得不少,看上去很糟糕。但它实际上做的主要事情是:

  • 通过构造函数 __CCCObject__init_block_impl_0 构造一个 block 对象,构造时候需要将实际业务函数 ***_func_0 作为其中一个参数传入;
  • 然后通过这个对象中的函数指针,来调用业务函数,需要将这个 block 结构自身作为参数传入;


那么如果没有修改 self 的属性,而是单纯的将 self 作为值传递到一个局部变量中呢:

1
2
3
4
5
6
7
8
9
10
11
- (id)init
{
if (self = [super init]) {
VoidBlock block = ^() {
CCCObject *cccObject = self;
};
block();
}

return self;
}


image


同样会强引用 self。

解除 block 引用循环

所以,如果在 block 中使用到 self 关键字(有例外,下详),而 self 通过某种方式引用了这个 block,那么引用循环就会产生。那么如何打破 block 的引用循环,经典的做法是:

1
2
3
4
5
6
7
8
9
10
11
12
- (id)init
{
if (self = [super init]) {
__weak __typeof(&*self) weakSelf = self;
VoidBlock block = ^() {
weakSelf.value = 1024;
};
block();
}

return self;
}


而 extobjc 这个库中的 EXTScope.h 定义了两个更方便的宏,可以更加优雅地解决 block 循环引用问题: weakifystrongify。最新版本的实现方法有点复杂,先看看传统的写法(以下内容主要参考自这里):

1
2
3
4
5
6
7
8
9
10
11
#define weakify( x ) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
autoreleasepool{} __weak __typeof__(x) __weak_##x##__ = x; \
_Pragma("clang diagnostic pop")

#define strongify( x ) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
try{} @finally{} __typeof__(x) x = __weak_##x##__; \
_Pragma("clang diagnostic pop")

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (id)init
{
if (self = [super init]) {
@weakify(self) // 定义了一个__weak的self_weak_变量
self.block = ^() {
@strongify(self) // 局域定义了一个__strong 的同名 self 指针指向self_weak
// 至于为什么没有报警告,看定义就知道了
self.value = 1024;
};
self.block();
}

return self;
}

原理就正如注释所写的,转换成原生代码的话:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (id)init
{
if (self = [super init]) {
__weak typeof(self) weakSelf = self;
self.block = ^() {
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf.value = 1024;
};
self.block();
}

return self;
}

解释一下:

  • 在 block 外部定义一个 weakSelf,供下面的 block 进行引用;
  • 在 block 中定义一个局部同名 self 指针,通过 weakSelf strong 引用自身,保证 block 执行的过程中,同名覆盖的变量(也就是 self)不会被释放掉;
  • 由于 block 对 self 是弱引用,在同名 strong 局部变量被释放之后,block 也就结束了所有对 self 的强引用;


image


至于新版本的实现变化,就是在原来的基础上,增加了对多个变量的支持,这里就不说了,这篇文章已有讲解@weakify, @strongify



以上

参考资料

weakify 以及 strongify

  1. iOS Proper Use of @weakify(self) and @strongify(self)
  2. Why you should start using @weakify and @strongify macros
  3. @weakify, @strongify
    block 原理
  4. 谈Objective-C block的实现
  5. block没那么难(一):block的实现
  6. block没那么难(二):block和变量的内存管理
  7. block没那么难(三):block和对象的内存管理
  8. 正确使用Block避免Cycle Retain和Crash
    内存管理
  9. iOS内存管理机制解析