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

http://pandara.xyz/2016/04/02/weakify_strongify/

什么是循环引用

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

a.objB = b;

b.objA = a;

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

(抄)如何更好地避免 block 循环引用_第1张图片

image

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

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

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

@interface A

//@property (nonatomic, strong) B b; //原来的代码

@property (nonatomic, weak) B b; //将 strong 引用改成 weak 引用

@end


(抄)如何更好地避免 block 循环引用_第2张图片

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

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


(抄)如何更好地避免 block 循环引用_第3张图片

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

@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 循环引用_第4张图片

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

Objective-C block的实现

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

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

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

//.h

@interface CCCObject : NSObject

@property (nonatomic) int value;

@end

//.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++ 代码:

clang -rewrite-objc CCCObject.m

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


(抄)如何更好地避免 block 循环引用_第5张图片

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 的值:

- (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 作为值传递到一个局部变量中呢:

- (id)init

{

if (self = [super init]) {

VoidBlock block = ^() {

CCCObject *cccObject = self;

};

block();

}

return self;

}


(抄)如何更好地避免 block 循环引用_第6张图片

同样会强引用 self。

解除 block 引用循环

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

- (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。最新版本的实现方法有点复杂,先看看传统的写法(以下内容主要参考自这里):

#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")

使用方法:

- (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;

}

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

- (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 的强引用;


(抄)如何更好地避免 block 循环引用_第7张图片

你可能感兴趣的:((抄)如何更好地避免 block 循环引用)