在閱讀這篇文章之前,首先思考如下問題:
- 為什么 weak strong dance 能夠避免循環(huán)引用?
- 為什么不直接使用weak?
- 使用 weak strong dance 的正確姿勢?
本文從 weak strong dance 的** 由來 用途 原理 擴(kuò)展** 逐步分析解答上述問題,但不僅僅只是解答問題。
由來
在iOS開發(fā)中,無論objective-c還是swift都是采用引用計(jì)數(shù)來管理內(nèi)存。而循環(huán)引用算是采用引用計(jì)數(shù)法管理內(nèi)存中比較棘手的一個問題。在MRC時代,因?yàn)閷ο蟮囊糜?jì)數(shù)是由程序猿自己來控制,優(yōu)秀的程序員能夠自如的把控對象之間的持有關(guān)系;到了ARC和swift中,由于編譯器接手了內(nèi)存管理的工作,為了方便程序員控制“對象之間的持有關(guān)系”,蘋果于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時,因?yàn)閎lock會捕獲外部變量,并將其拷貝到block中。ARC 下這里的變量如果是指針變量,會將指針變量所指向的對象的引用計(jì)數(shù)加一。 因此如果不是對block有一定理解很容易發(fā)生循環(huán)引用,在這篇文章里有介紹會發(fā)生循環(huán)引用的情景。
在RAC及swift中,為了避免block帶來的循環(huán)引用,官方推薦 weak strong dance ,即 在block外部聲明一個弱引用對象,在block內(nèi)部聲明一個局部變量強(qiáng)持有這個弱引用,通過使用新生成局部變量來避免循環(huán)引用。
原理
說到原理,得從block的內(nèi)部結(jié)構(gòu)說起:

其中invoke指向block的實(shí)現(xiàn)代碼,variables保存block捕獲到的外部變量。
現(xiàn)在來分析引言中的示例代碼:
- block 會將wself捕獲到variables中,因?yàn)槭莣eak修飾的,因此block不會對self進(jìn)行強(qiáng)引用;同時 block 中的 invoke (函數(shù)指針)會指向 block 中的實(shí)現(xiàn)代碼。
- 在執(zhí)行block時(即調(diào)用invoke),
TestViewController *sself = wself;才會執(zhí)行,如果執(zhí)行這行代碼時self還未釋放,那么這里會將TestViewController實(shí)例的引用計(jì)數(shù)加一,防止在調(diào)用self期間self已經(jīng)被釋放。當(dāng)這個方法執(zhí)行完,ARC會自動將引用計(jì)數(shù)減一。 - 如果在執(zhí)行block時,self已經(jīng)釋放,即wself被置為nil。那么
TestViewController *sself = wself;執(zhí)行時sself得到的也是一個nil,因此[sself dismissModalViewControllerAnimated:YES];將不會被執(zhí)行。 - 如果所有時候都和3中一樣只是“不執(zhí)行”,那該有多好。但是結(jié)果往往不如人意。不信? 測試代碼如下:
///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();
}
當(dāng) VcBlock 在執(zhí)行之前,obj已經(jīng)釋放,導(dǎo)致執(zhí)行 VcBlock 過程中 sObj 以及 sObj.block 均為nil。程序進(jìn)而crash在 sObj.block(); 這里。 真實(shí)情況往往比這里的模擬代碼復(fù)雜很多,可能會經(jīng)過幾次時間和空間的跨度;那么如何避免這種crash呢?
兩種處理:
- 1 、對
sObj.block進(jìn)行判斷
test_block block = ^{
WeakTestObject *sObj = wObj;
if (sObj.block) {
sObj.block();
}
};
- 2、 對
sObj進(jìn)行判斷
test_block block = ^{
WeakTestObject *sObj = wObj;
if (sObj) {
sObj.block();
}
};
顯然第二種處理更優(yōu)。首先餓哦們沒有必要對sObj的每一個舒心進(jìn)行判斷,其實(shí) 在使用sObj 時 ,往往也不是僅僅執(zhí)行它的一個block屬性,而且會涉及到block嵌套或其他各種坑爹情況,其次根據(jù)接口封閉原則我們也不應(yīng)該過多去關(guān)心類的實(shí)現(xiàn)。
最終 weak strong dance 的正確姿勢如下:
__weak __typeof(obj) wObj = obj;
test_block block = ^{
WeakTestObject *sObj = wObj;
if (sObj) {
/// do ...
}
};
擴(kuò)展
1. RAC中的宏
因?yàn)镽AC中大量使用block語言,為了方便開發(fā)者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
這里補(bǔ)充另一個坑(請注意如下兩種調(diào)用的區(qū)別)
//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其實(shí)是被重定義的局部的“self”, 而我們通過 _keyBoard 調(diào)用的話,表面上雖然看起來連self都沒有調(diào)用,更不會有什么問題了。但,第二種寫法其實(shí)是存在很大隱患的,系統(tǒng)在“尋找” _keyBoard 這個實(shí)例對象時,是通過對 self 指針進(jìn)行地址偏移得到的,在這里編譯器可不會對這個self進(jìn)行宏替換。
在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源碼中出現(xiàn)的都極少,不過 __unsafe_unretained 對比 __weak 來說在性能會比較好。
2. 接口設(shè)計(jì)原則
首先,并不是涉及到block引用外部對象的問題都會帶來循環(huán)引用;其次,如果是我們自己設(shè)計(jì)一個類的時候,應(yīng)該盡量的避免可能產(chǎn)生循環(huán)引用的問題(例如執(zhí)行完block后將其置nil),如果實(shí)在無法避免應(yīng)該在接口里詳細(xì)說明。
例如:蘋果框架中UIView封裝的 animateWithDuration 方法、GCD等都
不會帶來循環(huán)引用(注:NSTimer方法可能會帶來循環(huán)引用); 還有一些有名的三方框架例如 Masonry 也不會產(chǎn)生循環(huán)引用。
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)
})
上面是根據(jù)可選綁定方式得來的較常規(guī)的寫法,還有如下這種方式,可避免另外顯示生成strongSelf
testFunc(closure:{ [weak self] in
withExtendedLifetime(self, {
print(self ?? 0)
})
})
只是經(jīng) withExtendedLifetime 處理后的self 變?yōu)榱艘粋€可選類型。