在阅读这篇文章之前,首先思考如下问题:
- 为什么 weak strong dance 能够避免循环引用?
- 为什么不直接使用weak?
- 使用 weak strong dance 的正确姿势?
本文从 weak strong dance 的** 由来 用途 原理 扩展** 逐步分析解答上述问题,但不仅仅只是解答问题。
由来
在iOS开发中,无论objective-c还是swift都是采用引用计数来管理内存。而循环引用算是采用引用计数法管理内存中比较棘手的一个问题。在MRC时代,因为对象的引用计数是由程序猿自己来控制,优秀的程序员能够自如的把控对象之间的持有关系;到了ARC和swift中,由于编译器接手了内存管理的工作,为了方便程序员控制“对象之间的持有关系”,苹果于2011 WWDC Session #322中提出 weak strong dance ,官方示例代码如下
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:_observer];
}
- (void)loadView
{
[super loadView];
__weak TestViewController *wself = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
TestViewController *sself = wself;
[sself dismissModalViewControllerAnimated:YES];
}];
}
用途
在使用block时,因为block会捕获外部变量,并将其拷贝到block中。ARC 下这里的变量如果是指针变量,会将指针变量所指向的对象的引用计数加一。 因此如果不是对block有一定理解很容易发生循环引用,在这篇文章里有介绍会发生循环引用的情景。
在RAC及swift中,为了避免block带来的循环引用,官方推荐 weak strong dance ,即 在block外部声明一个弱引用对象,在block内部声明一个局部变量强持有这个弱引用,通过使用新生成局部变量来避免循环引用。
原理
说到原理,得从block的内部结构说起:
其中invoke指向block的实现代码,variables保存block捕获到的外部变量。
现在来分析引言中的示例代码:
- block 会将wself捕获到variables中,因为是weak修饰的,因此block不会对self进行强引用;同时 block 中的 invoke (函数指针)会指向 block 中的实现代码。
- 在执行block时(即调用invoke),
TestViewController *sself = wself;
才会执行,如果执行这行代码时self还未释放,那么这里会将TestViewController实例的引用计数加一,防止在调用self期间self已经被释放。当这个方法执行完,ARC会自动将引用计数减一。 - 如果在执行block时,self已经释放,即wself被置为nil。那么
TestViewController *sself = wself;
执行时sself得到的也是一个nil,因此[sself dismissModalViewControllerAnimated:YES];
将不会被执行。 - 如果所有时候都和3中一样只是“不执行”,那该有多好。但是结果往往不如人意。不信? 测试代码如下:
///WeakTestObject.h
typedef void(^test_block)(void);
@interface WeakTestObject : NSObject
/**
* <#summary#>
*/
@property (copy,nonatomic) test_block block;
@end
///ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
WeakTestObject *obj = [[WeakTestObject alloc]init];
obj.block = ^{
NSLog(@"execute block in obj");
};
__weak __typeof(obj) wObj = obj;
test_block VcBlock = ^{
WeakTestObject *sObj = wObj;
sObj.block();
};
obj = nil;
VcBlock();
}
当 VcBlock
在执行之前,obj已经释放,导致执行 VcBlock
过程中 sObj
以及 sObj.block
均为nil。程序进而crash在 sObj.block();
这里。 真实情况往往比这里的模拟代码复杂很多,可能会经过几次时间和空间的跨度;那么如何避免这种crash呢?
两种处理:
- 1 、对
sObj.block
进行判断
test_block block = ^{
WeakTestObject *sObj = wObj;
if (sObj.block) {
sObj.block();
}
};
- 2、 对
sObj
进行判断
test_block block = ^{
WeakTestObject *sObj = wObj;
if (sObj) {
sObj.block();
}
};
显然第二种处理更优。首先饿哦们没有必要对sObj的每一个舒心进行判断,其实 在使用sObj 时 ,往往也不是仅仅执行它的一个block属性,而且会涉及到block嵌套或其他各种坑爹情况,其次根据接口封闭原则我们也不应该过多去关心类的实现。
最终 weak strong dance 的正确姿势如下:
__weak __typeof(obj) wObj = obj;
test_block block = ^{
WeakTestObject *sObj = wObj;
if (sObj) {
/// do ...
}
};
扩展
1. RAC中的宏
因为RAC中大量使用block语言,为了方便开发者RAC中定义了一对宏 @weakify() @strongify()
,对于这对宏的具体分析可阅读哀殿的 这篇文章 ,文中提到“Xcode 丢失了错误提示的能力”这一问题
另外YYKit中也定义了类似的宏,同时避免了上述问题,如下
#ifndef weakify
#if DEBUG
#if __has_feature(objc_arc)
#define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object;
#endif
#else
#if __has_feature(objc_arc)
#define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object;
#endif
#endif
#endif
#ifndef strongify
#if DEBUG
#if __has_feature(objc_arc)
#define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object;
#endif
#else
#if __has_feature(objc_arc)
#define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object;
#endif
#endif
#endif
这里补充另一个坑(请注意如下两种调用的区别)
//1
[self.viewModel.resignSubject subscribeNext:^(id x) {
@strongify(self)
[self.keyBoard keyboardDown];
}];
//2
[self.viewModel.resignSubject subscribeNext:^(id x) {
@strongify(self)
[_keyBoard keyboardDown];
}];
在 1 中 self.keyBoard
中的self其实是被重定义的局部的“self”, 而我们通过 _keyBoard
调用的话,表面上虽然看起来连self都没有调用,更不会有什么问题了。但,第二种写法其实是存在很大隐患的,系统在“寻找” _keyBoard
这个实例对象时,是通过对 self 指针进行地址偏移得到的,在这里编译器可不会对这个self进行宏替换。
在RAC源码中还有对另一个宏 @unsafeify()
的使用
RACCompoundDisposable *selfDisposable = self.disposable;
[selfDisposable addDisposable:otherDisposable];
@unsafeify(otherDisposable);
// If this subscription terminates, purge its disposable to avoid unbounded
// memory growth.
[otherDisposable addDisposable:[RACDisposable disposableWithBlock:^{
@strongify(otherDisposable);
[selfDisposable removeDisposable:otherDisposable];
}]];
@unsafeify()
就是 __unsafe_unretained
在RAC中的宏,但是这种用法即使在RAC源码中出现的都极少,不过 __unsafe_unretained
对比 __weak
来说在性能会比较好。
2. 接口设计原则
首先,并不是涉及到block引用外部对象的问题都会带来循环引用;其次,如果是我们自己设计一个类的时候,应该尽量的避免可能产生循环引用的问题(例如执行完block后将其置nil),如果实在无法避免应该在接口里详细说明。
例如:苹果框架中UIView封装的 animateWithDuration
方法、GCD等都
不会带来循环引用(注:NSTimer方法可能会带来循环引用); 还有一些有名的三方框架例如 Masonry 也不会产生循环引用。
3. swift 中的 weak strong dance
testFunc(closure:{ [weak self] in
if let strongSelf = self {
// do something
print(strongSelf)
}
})
testFunc(closure:{ [weak self] in
guard let strongSelf = self else {return}
///do something
print(strongSelf)
})
上面是根据可选绑定方式得来的较常规的写法,还有如下这种方式,可避免另外显示生成strongSelf
testFunc(closure:{ [weak self] in
withExtendedLifetime(self, {
print(self ?? 0)
})
})
只是经 withExtendedLifetime
处理后的self 变为了一个可选类型。