什么是循环引用
先说循环引用,这个东西大概是大多数程序猿都应该知道的常识,不仅是 Objective-C ,很多其他语言也有这个概念。假设有两个类 A 与 B,A 中存在一个 ivar objB,B 中存在一个 ivar objA。现在各有一个 A 与 B 类型的对象: a 与 b,如果:1
2a.objB = b;
b.objA = a;
这样就会产生循环引用,如下图:
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
弱引用:weak类型的指针也可以指向对象,但是并不会持有该对象。并且在对象被释放的时候,自动被设置为 nil
那么 block 的循环引用也跟这个类似,只不过是其中一方变成一个 block:
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 中的断点来观察)。简单一点的情况编译器还能检查出来,然后给你一个警告:
要是引用层级深一点,编译器可就不知道了:
要知道 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 | //.m |
在 CCCObject 的 init 函数中定义了一个 block,这个 block 声明并初始化了一个局部变量,用 clang 将它翻译成 C++ 代码:1
clang -rewrite-objc CCCObject.m
我们可以得到一个拥有 10W+ 行代码的文件,摘取主要部分如下:
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 一下,摘取主要代码部分如下:
留意 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;
}
同样会强引用 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 循环引用问题: weakify
和 strongify
。最新版本的实现方法有点复杂,先看看传统的写法(以下内容主要参考自这里):1
2
3
4
5
6
7
8
9
10
11
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
autoreleasepool{} __weak __typeof__(x) __weak_
_Pragma("clang diagnostic pop")
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
try{} @finally{} __typeof__(x) x = __weak_
_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 的强引用;
至于新版本的实现变化,就是在原来的基础上,增加了对多个变量的支持,这里就不说了,这篇文章已有讲解@weakify, @strongify。
以上
参考资料
weakify 以及 strongify