天天看點

[譯]了解閉包中的記憶體洩漏

  • 原文位址: Understanding memory leaks in closures
  • 原文作者: Emilien Stremsdoerfer
  • 譯者: Isaac Pan

在初學者階段,開發過程中你甚至不知道會有記憶體洩漏的問題,完全忽略了他們,以至于最後發現代碼中到處都有這種問題,一籌莫展。

是以現在我們來深入了解一下記憶體洩漏什麼時候會出現,以及用什麼工具來避免它們。

Apple寫了一篇關于類之間的強引用和循環引用的不錯的文章,清晰易懂地解釋了什麼是記憶體洩漏,以及在一些情況下如何避免它們。但是文章講述的隻是不常出現的情況,并且也很容易辨認出來,關于閉包的部分寫的還是很困惑,是以讓我們來徹底搞清楚這個問題。

閉包中的循環引用

首先,你得明白什麼是閉包,它是幹什麼的。我喜歡把它稱為這樣的一小段代碼,當聲明過後,它會建立出一個臨時的類,包含所有它執行時所需要的對象的引用。

我們來從一個簡單的例子開始看起:一個包含CustomView的ViewController。CustomView中聲明了一個閉包,點選按鈕時,執行這個閉包。

class CustomView:UIView{ 
    var onTap:(()->Void)?
    ...
}

class ViewController:UIViewController{ 
    let customView = CustomView() 
    var buttonClicked = false
    
    func setupCustomView(){
        var timesTapped = 
        customView.onTap = {
            timesTapped +=  
            print("button tapped \(timesTapped) times")
            self.buttonClicked = true
        }
    }
}
複制代碼
           

當傳值給這個閉包時,它需要引用一些變量才能執行。在這裡,

self

timesTapped

這兩個閉包外的變量被引用了。為了確定這些變量在執行時能夠使用,閉包會強引用它們,這樣它們就不會在使用前被釋放,以至于崩潰。

但是仔細看看,ViewController強引用了CustomView,CustomView強引用了onTap這個閉包,而這個閉包卻強引用了

self

是以這時候的引用關系成了這樣:

從圖中我們可以清楚地看到循環引用。這意味着當你退出這個view controller時,它不會從記憶體中釋放掉,因為它仍然被這個閉包所引用。

這個例子十厘清楚,

viewController

中包含一個

subview

屬性,

subview

又包含一個捕獲了

self

onTap

閉包。但不幸的是,還有更複雜的情況。

潛在的循環引用

有一個問題需要你不斷地提醒自己:誰持有了這個閉包?

UITableView

如果寫過iOS app,你應該已經知道了在一些情況下怎麼去使用UITableView了,并且大多數時候用的還是自定義的cell和button

下面是如何使用swift來實作。首先建立一個CustomCell,這個cell定義了一個用于執行點選事件的閉包:

class CustomCell: UITableViewCell {
  
  @IBOutlet weak var customButton: UIButton!
  var onButtonTap:(()->Void)?
    
  @IBAction func buttonTap(){
      onButtonTap?()
  }
}
複制代碼
           

然後在ViewController中去實作這個閉包的功能:

class TableViewController: UITableViewController {

  override func tableView( tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
      cell.onButtonTap = {
          self.navigationController?.pushViewController(NewViewController(), animated: true)
      }
  }
}
複制代碼
           

誰持有了這個閉包?由于我們在CustomCell中明确聲明了,是以在這裡,我們清楚地知道是這個cell,并且tableView持有了cell,tableViewController也持有了tableView。

如下圖所示,循環引用又出現了。并且如果你之前沒有碰到這樣的情況,這次的循環引用更難被發現。

GCD

相信你之前已經用到過GCD了,你清楚下面代碼中是否有循環引用嗎?

override func viewDidLoad() {
  super.viewDidLoad()
  DispatchQueue.main.asyncAfter(deadline: .now() + ) {
    self.navigationController?.pushViewController(NewViewController())
  }
}
複制代碼
           

首先我們來搞清楚,誰持有了這個閉包?ViewController并沒有任何相關的屬性,它隻是在一個DispatchQueue的單例中被調用了。是以這時,最壞的情況發生了,DispatchQueue的單例在調用

asyncAfter

方法時持有了它。遺憾的是我們不能看到這個方法具體的實作,但是,這個閉包隻會執行一次,并且是在一個事先明确的時間執行,這個單例沒有理由保留這個引用關系。在這種情況下,當閉包執行完畢後,對

self

的引用就結束了,而

self

又沒有引用這個閉包,是以,并沒有循環引用。請注意,使用

UIView.animate(){}

閉包實作動畫同樣适用這個邏輯。

Alamofire

我們來看看這種情況,我們需要實作一個App,有一個LoginViewController,我們需要用Alamofire來與伺服器互動資料:

Alamofire.request("https://yourapi.com/login", method: .post, parameters: ["email":"[email protected]","password":"1234"]).responseJSON { (response:DataResponse<Any>) in
    if response.response?.statusCode ==  {
        self.navigationController?.pushViewController(NewViewController(), animated: true)
    }else{
        //Show alert
    }
}
複制代碼
           

誰持有了這個閉包?在這裡,閉包是作為

request

函數的參數聲明的,但是你并不知道Alamofire在閉包中做了什麼,也不知道閉包什麼時候被釋放的。

如果你去深究一下實作的原理,你就能了解到,

request

方法有一個操作隊列

queue

。當

response()

方法被調用時,我們會把這個閉包放入

queue

中,當閉包執行完畢時,閉包就會從

queue

中移除。是以,在這裡并沒有循環引用發生,因為隻有

queue

保留了閉包,但一旦執行完畢,閉包就立刻被釋放了。

注意,即使你保留了

request

的引用,或者引用

SessionManager

,閉包也會被釋放掉,不會有任何循環引用。

RxSwift

在這個例子裡,你需要實作一個UISearchBar,當你改變

searchBar

中的文字時,label同時改變:

class ViewController: UIViewController {
  
  @IBOutlet weak var searchBar: UISearchBar!
  @IBOutlet weak var label: UILabel!
  
  override func viewDidLoad() {
    searchBar.rx.text.throttle(, scheduler: MainScheduler.instance).subscribe(onNext: {(searchText) in
      self.label.text = "new value: \(searchText)"
    }).addDisposableTo(bag)
  }
}
複制代碼
           

誰持有了這個閉包?這個閉包能被多次調用,并且我們也不知道什麼時候被調用,是以RxSwift需要保持對閉包的引用。在這種情況下閉包實際上是被

searchBar

直接持有的,因為當

searchBar

被釋放時,閉包也一定要被釋放。但是仔細看看,

self

持有了

searchBar

,閉包又引用了

self

。是以在這裡是存在循環引用的,我們需要打破這個引用以防記憶體洩漏。

打破循環引用

要打破循環引用,你隻需要破壞其中的一個引用關系即可,我們當然要選擇最簡單的。當處理閉包問題時,我們期望能打破其中最後一個連接配接,那就是閉包的引用。

要實作這一點,你需要明确,當你的閉包捕獲外部變量時,最好不要是一個強引用關系。你可以有兩個選擇,在閉標頭部使用

weak

或者

unowned

關鍵字。

例如,在上面的UITableView例子中:

cell.onButtonTap = { [unowned self] in
    self.navigationController?.pushViewController(NewViewController(), animated: true)
}
複制代碼
           

是用

weak

還是

unowned

呢?這有一點複雜。通常來說,當閉包不會比它所捕獲的變量存在得久時,你需要使用

unowned

。在上面的例子中,cell和閉包不會比

tableViewController

更“持久”,是以我們可以使用

unowned

。如果你想知道更多關于

weak

unowned

的用法,我推薦閱讀一下這些非常棒的文章

Unowned or Weak? Lifetime and Performance

"WEAK, STRONG, UNOWNED, OH MY!" - A GUIDE TO REFERENCES IN SWIFT。

記憶體洩漏的調試

有時候,你會發現想要搞清楚閉包是否被引用是很困難的,特别是你使用了第三方的一些庫或者一些私有聲明的時候。是以,你需要通過調試來找出循環引用。Xcode提供了非常好用的工具來幫助你找到記憶體洩漏。打開你App的工程,點選Xcode底部如下圖所示的小圖示,你就能檢視記憶體情況。

還是上面的TableView例子中,如果在閉包

onButtonTap

中你不使用

weak

或者

unowned

關鍵字,你就會發現下圖所示的情況:

右側的感歎号代表發生了記憶體洩漏。但有時候,Xcode并不能正常地發現洩漏,洩漏真實存在但并沒有被發現是完全有可能的。在這種情況下,你隻需要關注記憶體中有哪些東西,如果你發現了一些本不應該存在的東西,很有可能就發生了洩漏。

這篇文章能幫助到你更加深刻的了解在閉包中記憶體洩漏時如何發生的,希望你能喜歡。如果有任何疑問或者回報,盡情寫下來吧!

特别感謝Rémy Virin,在他對本文主題持有深刻了解的幫助下,我完成了這篇文章。

轉載于:https://juejin.im/post/5b0e728e518825157914ffcf