(抄)如何更好地避免 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;

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

image

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

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

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

@interface A

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

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

@end


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

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


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 为什么会引用了 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+ 行代码的文件,摘取主要部分如下:


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;

}


同样会强引用 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 的强引用;


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容