你真的懂 weak strong dance 嗎?

在閱讀這篇文章之前,首先思考如下問題:

  1. 為什么 weak strong dance 能夠避免循環(huán)引用?
  2. 為什么不直接使用weak?
  3. 使用 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)說起:


block內(nèi)部結(jié)構(gòu)

其中invoke指向block的實(shí)現(xiàn)代碼,variables保存block捕獲到的外部變量。

現(xiàn)在來分析引言中的示例代碼:

  1. block 會將wself捕獲到variables中,因?yàn)槭莣eak修飾的,因此block不會對self進(jìn)行強(qiáng)引用;同時 block 中的 invoke (函數(shù)指針)會指向 block 中的實(shí)現(xiàn)代碼。
  2. 在執(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ù)減一。
  3. 如果在執(zhí)行block時,self已經(jīng)釋放,即wself被置為nil。那么 TestViewController *sself = wself; 執(zhí)行時sself得到的也是一個nil,因此 [sself dismissModalViewControllerAnimated:YES]; 將不會被執(zhí)行。
  4. 如果所有時候都和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)榱艘粋€可選類型。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容