Swift中的Weak/Strong Dance

weixin_33816300發表於2016-05-26

馬上又要過年了,誒,再也不能像當初那樣無恥地逗利是了(我們廣東的方言討紅包的意思)

圖1

626796-65d1f5f4eba454a3.jpg

圖2

626796-1ac77b6d85f95cd8.jpg

看來今年沒利了

誰讓哥已經工作了呢。

公司今年的開發任務算是完結了,蘋果又極不負(hǎo)責(yàng)任(de)地放聖誕不稽核了,所以這半個月就該清閒下來了。

博主掐指一算,Swift已經養到2.1了,並且也開源了,這樣看來Swift也夠肥了,語法也絕逼不會再有大改動了,是該再次抓起來了。

為什麼說再呢,其實在當初Swift Beta版本的時候,我們專案經理嘗試了一下palyground後,一拍手,棒棒噠,"我們用Swift開發接下來的專案吧",然後就是Swift1.0 … 2.0,每一次升級,每一次語法更迭,每一次XCode開啟的那一刻都是滿江紅,那觸目驚心的場面無數次讓博主受盡折磨。最後慶幸的是,這個專案死了,哦耶!

好吧,重新撿起Swift吧,今天的這篇文章是一篇激情翻譯大作。

好吧回到博文的主題中來,這次我們說說“Weak/Strong Dance”

在block中解決迴圈引用要追尋的2011 WWDC Session #322{:target="_blank"} 當時驚豔的程式碼是這樣的:

- (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];
  }];
}

或者可以看看AFNetWorking{:target="_blank"} 是這樣用block的:

__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};

我們都很熟悉Objective-C中的“weak/strong dance”,但是寂寞無聊的我突然就很想知道Swift語言中該怎麼做呢?是否存在傳說中的最佳實踐呢?

好啦,翻譯開始!

原文{:target="_blank"}

首先,我們祭出一個在閉包中沒有使用weak的引用導致的迴圈引用的例子

class C {
    let name: String
    var block: (() -> Void)?

    init(name: String) {
        self.name = name
        block = {
            self.doSomething()
        }
    }
    deinit { print("Destroying \(name)") }
    func doSomething() { print("Doing something for \(name)") }
}

var c = C(name: "one")
print(c.name)
c = C(name: "two")
print(c.name)

輸出

one
two

這是一個巨基礎又明顯的迴圈引用的例子,self -> block ->self

所以,deinit 方法是絕逼不會被執行的,即使你把 c 重新指向nil或者其他的例項,c也不會被銷燬,這就是頑固又調皮的迴圈引用了,尤其是當你把c指向nil之後,這個物件你就再也引用不了了,它就靜靜的躺在堆記憶體裡面,遺世而獨立,然後你就堆記憶體洩露了,然後你就淡淡的憂傷從下體傳來 ~ ~沒有然後了

其實Swift中閉包的引數列表(Capture List{:target="_blank"}) 已經能夠很好的讓你獲取一個weak self來避免迴圈引用了,但這還達不到我們的要求,只有weak是構不成“weak/strong dance”滴。

使用閉包引數列表

class C {
    let name: String
    var block: (() -> Void)?

    init(name: String) {
        self.name = name
        block = { [weak self] in  // <-- 這裡做出一些改變
            self?.doSomething()
        }
    }
    deinit { print("Destroying \(name)") }
    func doSomething() { print("Doing something for \(name)") }
}

var c = C(name: "one")
print(c.name)
c = C(name: "two")
print(c.name)

輸出

one
Destroying one
two

這樣就沒有迴圈引用啦~

在閉包中使用self

使用 [weak self]有一個細節,就是self在閉包中會變成Optional 從上面的程式碼中self?.doSomething() 就可以看出來了。

但是如果你在這個閉包中狂轟亂炸的使用self? (多次使用self?),問題就來了,因為這個self?是一個弱引用的,那麼你沒法確定在這個閉包中所有的self?操作都能執行完畢,畢竟若引用的self可能隨時都掛掉,然後怒舉一個栗子:

圖4

626796-9a2484c9aae5dfcc.jpg
class C {
    deinit { println("Destroying C") }
    func log(msg: String) { println(msg) }
    func doClosure() {
        dispatch_async(dispatch_get_global_queue(0, 0)) { [weak self] in
            self?.log("before sleep")
            usleep(500)
            self?.log("after sleep")
        }
    }
}

var c: C? = C()  // Optional, so we can set it to nil
c?.doClosure()

dispatch_async(dispatch_get_global_queue(0, 0)) {
    usleep(100)
    c = nil  // This will dealloc c
}

dispatch_main()

輸出

before sleep
Destroying C

小提示:當然一般來說在dispatch_async()中你不必擔心會有迴圈引用,因為self並不會持有dispatch_async()的block,所以上述的程式碼中並不會真的導致迴圈引用,如果你的閉包並不是很注重結果的,那麼selfnil閉包就不會再執行,這個還是挺有用的。

上述的程式碼中不會列印after sleep,因為self?在列印這句話之前已經掛掉了。

通常這種無根之源的bug可以把你整的半死。所以通常遇到這種閉包中多次試用self?的操作的時候,一般會把self?變為又粗又壯的strong self,(博主也是又粗又壯的,捂臉~~)這就是傳說中的“weak/strong dance”,這個舞蹈,額,什麼鬼,為什麼把這個技術叫做dance啊,我覺得叫做美隊解禁奧義技還不錯,婦聯裡面的美國隊長也是由weak變成strong的嘛~,好吧,扯太遠,菊花都扯疼了,我們這是在技術翻譯呢!要嚴肅!要尊重原作者!我們還是叫dance吧,有了這個dance之後呢,我們就能確保一旦閉包被執行,self就不會為nil

但是,就像文章開頭說的,對於在Swift的“weak/strong dance”中變回strong的這部分的最佳實踐是什麼我也不是很確定的。。。

獲取強引用的一些想法

使用可選繫結if let

func doClosure() {
    dispatch_async(dispatch_get_global_queue(0, 0)) { [weak self] in
        if let strongSelf = self {  // <-- 這裡就是精髓了
            strongSelf.log("before sleep")
            usleep(500)
            strongSelf.log("after sleep")
        }
    }
}

// or in Swift 2, using `guard let`:
dispatch_async(dispatch_get_global_queue(0, 0)) { [weak self] in
    guard let strongSelf = self else { return }  // <-- 這裡就是精髓了
    strongSelf.log("before sleep")
    usleep(500)
    strongSelf.log("after sleep")
}

輸出

before sleep
after sleep
Destroying C

優點:

  • 很明顯的看出整個操作的流程
  • 在閉包中拿到了非可選的本地變數

缺點:

  • 很不幸的是我們不能if let self = self,因為self是常量,不可變,這樣的話我們就只能if let strongSelf = self 在閉包的作用域中都要使用醜陋的strongSelf了。
  • 在swift的閉包中,如果你沒有試用strongSelf而是使用了self,這樣編譯器會警告!因為這個時候self是可選的嘛,相比較OC中,就不會警告了。(這句話哥讀了21遍,為什麼覺得這個不是缺點呢)

使用withExtendedLifetime

在Swift的標準庫中有一個函式:withExtendedLifetime(),感覺就像Apple這個金魚佬故意誘導我們使用這個函式來實現“weak/strong dance”。

/// Evaluate `f()` and return its result, ensuring that `x` is not
/// destroyed before f returns.
func withExtendedLifetime<T, Result>(x: T, @noescape _ f: () -> Result) -> Result

那就試試

func doClosure() {
    dispatch_async(dispatch_get_global_queue(0, 0)) { [weak self] in
        withExtendedLifetime(self) {
            self!.log("before sleep")
            usleep(500)
            self!.log("after sleep")
        }
    }
}

優點:

  • 閉包中不再需要使用醜陋的strongSelf

缺點:

  • self還是他媽可選的,呼叫方法什麼的還是要!?,還是要解包,博主突然想起自己的一個技能:單手解,呵呵

自定義一個withExtendedLifetime()

這個方法是 @jtbandes{:target="_blank"} 這哥們想的,大概會是這樣:

 extension Optional {
    func withExtendedLifetime(body: Wrapped -> Void) {
        if let strongSelf = self {
            body(strongSelf)
        }
    }
}

// Then:
func doClosure() {
    dispatch_async(dispatch_get_global_queue(0, 0)) { [weak self] () -> Void in
        self.withExtendedLifetime {
            $0.log("before sleep")
            usleep(500)
            $0.log("after sleep")
        }
        return
    }
}

優點:

  • Follows naming conventions set by the standard library.(原文) 感覺沒優點~

缺點:

  • strongSelf變成了使用$0 ,博主認為哦,還是很醜陋,並且可讀性更差了
  • In this case, I had to add some extra type info to the dispatch_async() closure. I’m not totally sure why. 不知道他說什麼鬼~

翻譯至此結束了

後記

關於Swift中 “Weak/Strong Dance”,中的Weak部分,大家可以參閱喵大的這篇文章 記憶體管理,weakunowned{:target="_blank"} 。

用回了一個多禮拜的Swift真是感受頗多,雖然Xcode在寫Swift還是像純文字編輯器一樣,但是我還是想說一句:Swift真™安全!想crash都難咯。

收筆,走人。

相關文章