一篇文章搞懂 @weakify 和 @strongify
一、引言
常常在某些项目或第三方库代码中见到 @weakify(self);
和 @strongify(self);
这样的代码,回忆起 Objective-C 的语法,好像没有这样的用法啊。这让我一头雾水,充满疑惑。经过一番调研后恍然大悟,于是总结成本文于此。
二、循环引用
相信读到本篇文章的读者,对 Objective-C 内存管理原理和循环引用等概念已有一定理解,这里只做简单叙述。Objective-C 通过引用计数(Reference Count)来管理堆内存。当我们创建对象时,系统会为该对象分配一块内存,Objective-C 会维护该对象的引用计数,当该对象的引用计数大于 0 时,其所在内存不会被系统回收;当引用计数为 0 时,其所在内存会被系统回收,该对象被销魂。可以看出,引用计数是内存管理的关键。
假设一种场景,A 对象强引用了 B 对象,那么即使没有别的对象对 B 进行强引用,B 的引用计数至少为 1;如果此时 B 对象也对 A 进行了强引用,那么 A 对象的引用计数也至少为 1,A、B 的引用计数都将永远不会降低到 0,也就是永远不会被释放,如果 A、B 对象不再被使用,没有任何别的对象引用它们,这将导致为 A、B 所分配的内存泄漏。这就是循环引用导致的内存泄漏。
一种常见的循环引用案例就是 block 属性:假设一个对象有一个强引用的 block 属性,同时此 block 通过 self 指针访问了该对象的其它属性,这将导致 block 也对该对象进行了强引用。示例代码如下:
本例中的问题将导致 SecondViewController
对象内存泄漏,即使其被弹出,不再被使用,为其分配的内存仍不会被释放。
三、使用 __weak 消除循环引用
那如何解决这个问题呢?这就引出了我们的主角之一 __weak 关键字。在 block 中不直接使用 self 指针,而是使用一个被 __weak 关键字修饰的指针,此时 block 对 self 是弱引用引用,打破了循环引用的闭环。修正后的代码如下:
四、使用 __strong 避免 block 代码执行一半,self 指向的对象被释放
到目前为止,我们使用了关键字 __weak,问题好像已经被轻松地解决了。对于一般情况,确实是这样。
考虑如下这样定义 block 代码呢?
在新定义的 block 中,NSLog 函数并不会在调用 block() 时立即执行,而是延迟一秒执行。一秒后,可能 self 所指向的对象已经被释放,NSLog 的输出将不能满足我们的需求。
那能不能做到既避免循环引用,而又避免输出异常呢?
答案是可以的,只要我们的对 block 代码进行如下修改:
可以看到,我们在 block 里定义了 self_strong_ 指针,它和 self_weak_ 相反,能够对指向的对象进行强引用。这将意味着,只要开始执行 block 代码,直到 block 代码执行完毕前,self 所指向的对象不会被释放,NSLog 函数能输出正确的结果。
这就达到了完美的结果,用 __weak 指针解除了循环引用,避免内存泄漏。用 __strong 修饰的指针 self_strong_ 对 self_weak_ 进行强引用,一旦 block 开始执行后,直到 block 执行完毕,self 所指向的对象不会被释放,NSLog 输出正确的结果。
五、宏定义 weakify 和 strongify
那 @weakify(self)
和 @strongify(self)
又是怎么回事呢?
weakify 和 strongify 是两个宏定义,最早出现在库 libextobjc 中,后被广泛采用,一般定义如下:
可以看出,这两个宏定义对于测试环境和正式环境,ARC 环境和非 ARC 环境分别进行了定义,先将它们分类整理如下:
可以看到,在宏定义中,在指针之前,测试环境加入了代码 autoreleasepool {}
,正式环境加入了代码 try {} @catch (...){}
。加入它们的目的是为了在使用 weakify 或 strongify 时,吃掉前面加上的 @ 符号。
在 autoreleasepool {}
前加上 @,刚好为 @autoreleasepool {}
;在 try {} @catch (...){}
前加上 @,刚好为 @try {} @catch (...){}
,符合 Objective-C 语法。参见链接。
在 Debug、ARC 环境中,代码 @weakify(self)
等价于 @autoreleasepool {} __weak __typeof__(self) self_weak_ = self;
。
在 Debug、ARC 环境中,代码 @strongify(self)
等价于 @autoreleasepool {} __typeof__(self) self = self_weak_;
。
你可能会问,那为什么测试环境和正式环境吃掉 @ 的代码还不一样呢?在测试环境使用 @autoreleasepool {}
,而不使用 @try {} @catch (...){}
,是因为空的 try catch
代码会产生编译器警告。在正式环境使用 @try {} @catch (...){}
而不使用 @autoreleasepool {}
,是因为 @autoreleasepool {}
创建 autorelease pool 会增加系统开销。
六、使用 weakify 和 strongify
直接给出使用示例如下:
在 Debug、ARC 环境,代码 @weakify(self)
,在预编译阶段会被替换成代码:
在 Debug、ARC 环境,代码 @strongify(self)
,在预编译阶段会被替换成代码:
不难看出,在 block 外, @weakify(self)
定义了 __weak 类型的指针 self_weak_;在 block 内 @strongify(self)
使用 self_weak_ 来定义 __strong 类型的指针 self。值得注意的是,在 block 作用域内定义的 self 将覆盖作用域更广的,类里内置的 self 指针。因此在 @strongify(self)
之后,直接使用 self 指针并不会导致循环引用。
另外,除了可以对 self 指针使用 weakify 和 strongify,对其它普通指针一样可以使用。如:
七、总结
weakify
、strongify
为两个宏定义,使用它们是为了解决使用 block 时常遇到的两个问题:
在属性 block 中引用其它兄弟属性导致循环引用的问题(使用
@weakify
中定义的 weak 指针);在开始执行 block 代码后,block 所引用的对象被释放,导致 block 执行结果偏离预期(在 block 中使用
@strongify
所定义的 strong 指针)。
在 weakify
、strongify
宏定义中,还使用了 autoreleasepool {}
和 try {} @catch (...){}
,目的是为了在使用 weakify、strongify
时能够在前面加上 @ 符号。其中测试环境使用 autoreleasepool,避免空 try catch 带来的编译警告;在正式环境使用 try {} @catch (...){}
,避免创建 autoreleasepool 增加系统开销。
如读完本文仍有疑问,欢迎评论区留言,留言必回。
版权声明: 本文为 InfoQ 作者【疯清扬】的原创文章。
原文链接:【http://xie.infoq.cn/article/903068956d7c2a5952059b16c】。文章转载请联系作者。
评论