內存泄漏在 iOS 中是永恆的話題,若是你在開發過程當中不當心對待的話,那麼總有一天他會以 Crash 的形式提醒你它的存在。內存泄漏不只破壞用戶體驗,並且會影響性能甚至應用的安全。既然內存泄漏如此的重要,因此這篇文章在這篇文章將說一說 Swift 閉包中的內存泄漏問題。html
Apple 在文章中詳細介紹了循環強引用的概念、何爲內存泄漏、如何避免。可是文章中的實例太過於簡單,在真正的應用過程當中狀況遠比這個複雜,接下來的內容就是介紹其中最爲複雜的閉包中的泄露分析。api
首先,咱們須要清楚的理解閉包的概念:閉包是自包含的函數代碼塊,能夠在代碼中被傳遞和使用。簡單來講:閉包是一段可執行的代碼塊而且它能自動捕獲上下文的變量和常量,而後在須要的時候被執行。詳細內容可參見地址。安全
咱們從這個簡單的實例開始:ViewController 中有一個 CustomView 類型的成員屬性變量,同時 CustomView 有一個點擊事件的閉包函數 onTap :網絡
class CustomView:UIView{ var onTap:(()->Void)? ... } class ViewController:UIViewController{ let customView = CustomView() var buttonClicked = false func setupCustomView(){ var timesTapped = 0 customView.onTap = { _ in timesTapped += 1 print("button tapped \(timesTapped) times") self.buttonClicked = true } } }
在給閉包函數 onTap 賦值的語句中咱們對 buttonClicked 進行了賦值,這就致使了對 self 的強引用。可是咱們仔細思考後就不難發現其中的問題: self 引用了 customView 變量,而後 customView 變量的飲用了 onTap 閉包,最後 onTap 閉包引用了 self 。其結果相似下圖:閉包
上圖中你能清晰的看見循環結構,這致使程序退出的時候不能正常的銷燬內存致使內存泄漏的發生。app
除了上面那種明顯的循環引用有些閉環隱藏的更深也更隱蔽。解決這個問題的關鍵就是:在對閉包賦值的時候問本身誰是閉包的擁有者,而後向上溯源到根節點。異步
下面咱們來看最多見 UITableView 中隱藏的循環(最多見的每每越容易被忽略)。通常狀況下咱們都是在 UIViewController 中新建 UITableView 實例少數狀況下也會使用 UITableViewController ,可是無論哪一種情形咱們都會新建自定義的 UITableViewCell 。async
下面的代碼中咱們新建了一個名爲 CustomCell 的 UITableViewCell 子類,該類中包含了一個 UIButton實例屬性以及按鍵點擊事件的閉包屬性 onButtonTap。ide
class CustomCell: UITableViewCell { @IBOutlet weak var customButton: UIButton! var onButtonTap:(()->Void)? @IBAction func buttonTap(){ onButtonTap?() } }
而後咱們在 ViewController 對該閉包賦值:函數
class ViewController: UITableViewController { ... override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell cell.onButtonTap = { _ in self.navigationController?.pushViewController(NewViewController(), animated: true) } } }
這裏咱們對 onButtonTap閉包進行溯源:誰擁有該閉包?毫無疑問是 CustomCell類的實例 cell。而 cell 又是屬於 tableView,tableView又屬於 self 所表明的UITableViewController 實例。
正以下圖表現的那樣,這裏也有一個循環引用,只不過度析路線更長因此顯得更隱蔽。
若是你之前用過 GCD 的話,那麼你能一眼判斷下面代碼是否有循環引用。
override func viewDidLoad() { super.viewDidLoad() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.navigationController?.pushViewController(NewViewController()) } }
一樣的使用溯源法分析閉包:擁有閉包的對象是 DispatchQueue 單例,該單例並不被 ViewController 任何屬性所引用,但DispatchQueue 單例的閉包中卻持有了 self。雖然咱們不知道該單例的具體實現,可是咱們清楚該異步閉包會在2s後被執行一次,執行完成以後該閉包就會釋放對 self 的引用。因此咱們由此能夠判定這段閉包代碼是不存在循環引用問題的。
這部分的代碼邏輯和分析一樣適用於 UIView 的動畫閉包函數中
Alamofire 能夠說是 Swift 網絡處理中最經常使用的第三方庫了,其中的請求處理中一樣涉及到閉包函數。下面這段代碼是請求登錄接口:
Alamofire.request("https://yourapi.com/login", method: .post, parameters: ["email":"test@gmail.com","password":"1234"]).responseJSON { (response:DataResponse<Any>) in if response.response?.statusCode == 200 { self.navigationController?.pushViewController(NewViewController(), animated: true) } else { //Show alert } }
上訴代碼中的閉包又是屬於哪一個對象?這裏咱們須要深刻 Alamofire 的實現中去探尋。首先 request方法會返回一個 DataRequest類型對象,而該對象的 responseJSON方法中將閉包做爲參數 completionHandler傳入,最後該閉包存入了 OperationQueue 類型的隊列 queue 中,閉包執行完成後會自動從隊列中移除。由此咱們可知:閉包被 queue所持有而且一次執行後就移除了,此處不存在循環引用。
爲了打破循環引用帶來的內存泄漏問題,根本途徑就是破壞該循環,將某個對象對另外一個對象的強引用去除。在閉包環境的循環問題,咱們都傾向於將閉包中的強引用去除,畢竟這簡單並且看起來更直觀。
爲了實現該目的,咱們在閉包捕獲的上下文變量中作文章。咱們使用關鍵詞 weak、unowned 來打破循環。例如上文中提到的 UITableView :
cell.onButtonTap = { [unowned self] in self.navigationController?.pushViewController(NewViewController(), animated: true) }
上訴兩個關鍵詞存在着明顯的區別 weak 是可選值而 unowned 則必定不爲可選值,換句話說 weak 關鍵詞所指對象可能爲 nil 而 unowned 則必定不能是 nil,所以在選用的時候須要認真考慮一下。通常來講若是閉包生命週期不長於其捕獲的上下文變量的生命週期咱們會使用 unowned,不然咱們選擇 weak 。
上面咱們分析了大部分閉包中的循環引用問題,咱們得知並非全部的狀況下都會致使內存泄漏。若是在咱們使用了第三方庫尤爲是一些私有實現庫的狀況下,這部分的分析在代碼層面將變的很困難而且工做量很大。好在Xcode爲咱們提供的調試工具,在工程運行的狀況下,咱們在調試區域能夠找到以下圖所示按鍵:
在 UITableView 的示例中,若是咱們移除閉包中的 unowned 或者 weak 的話,你就能在左側看見下圖
上圖中的左側感嘆號代表了這裏存在着內存泄漏的狀況,這樣你就要去查看代碼了。固然你又內存泄漏可是沒有感嘆號標記的狀況也是徹底有可能的,此時你就要啓用內存分析工具了而且分析內存中的對象,這些對象是否應該存在。