如何在 Swift 中優雅的處理閉包致使的循環引用

Objective-C 做爲一門資歷很老的語言,添加了 Block 這個特性後深受廣大 iOS 開發者的喜好。在 Swift 中,對應的概念叫作 Closure,即閉包。雖然更換了名字,可是概念和用法仍是類似的,就算是反作用也同樣,有可能致使循環引用。git

下面咱們用一個例子看一下,首先咱們須要第一個控制器(FirstViewController),它所作的就是簡單的推出第二個控制器(SecondViewController)。github

class FirstViewController: UIViewController {
    
    private let button: UIButton = {
        let button = UIButton()
        button.setTitleColor(UIColor.black, for: .normal)
        button.setTitle("跳轉到 SecondViewController", for: .normal)
        button.sizeToFit()
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        button.center = view.center
        view.addSubview(button)
        button.addTarget(self, action: #selector(buttonClick), for: .touchUpInside)
    }
    
    @objc private func buttonClick() {
        let secondViewController = SecondViewController()        
        navigationController?.pushViewController(secondViewController, animated: true)
    }
}
複製代碼

下面是 SecondViewController 的代碼。SecondViewController 所作的事情是推出第三個控制器(ThirdViewController),不一樣的是,thirdViewController 是做爲一個屬性存在的,同時它還有一個閉包 closure ,這是咱們用來測試循環引用問題的。還實現了 deinit 方法,用來打印一條語句,看該控制器是否被釋放了。bash

class SecondViewController: UIViewController {
    
    private let thirdViewController = ThirdViewController()
    private let button: UIButton = {
        let button = UIButton()
        button.setTitleColor(UIColor.black, for: .normal)
        button.setTitle("跳轉到 ThirdViewController", for: .normal)
        button.sizeToFit()
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        button.center = view.center
        view.addSubview(button)
        button.addTarget(self, action: #selector(buttonClick), for: .touchUpInside)
    }
    
    deinit {
        print("SecondViewController-被釋放了")
    }
    
    @objc private func buttonClick() {
        thirdViewController.closure = {
            self.test()
        }
        navigationController?.pushViewController(thirdViewController, animated: true)
    }
    
    private func test() {
        print("調用 test 方法")
    }
}

複製代碼

接下來咱們看一下 ThirdViewController 的代碼。在 ThirdViewController 中有一個按鈕,點擊一下就會觸發閉包。同時咱們還實現了 deinit 方法,用來打印一條語句,看該控制器是否被釋放了。網絡

class ThirdViewController: UIViewController {
    
    private let button: UIButton = {
        let button = UIButton()
        button.setTitleColor(UIColor.black, for: .normal)
        button.setTitle("點擊按鈕", for: .normal)
        button.sizeToFit()
        return button
    }()
    
    var closure: (() -> Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        button.center = view.center
        view.addSubview(button)
        button.addTarget(self, action: #selector(buttonClick), for: .touchUpInside)
    }
    
    deinit {
        print("ThirdViewController-被釋放了")
    }
    
    @objc private func buttonClick() {
        closure?()
    }
}

複製代碼

當咱們連續推到第三個控制器,點擊按鈕(觸發閉包)後,再回到第一個控制器,看一下三個控制器的生命週期。當流程走完後,發現控制檯只有一條語句:閉包

調用 test 方法
複製代碼

這說明閉包已經引發了循環引用問題,致使第二個控制器沒能被釋放(內存泄漏)。正是由於閉包會致使循環引用,因此在閉包中調用對象內部的方法時,都要顯式的使用 self,提醒咱們要注意可能引發的內存泄漏問題。與 Objective-C 不一樣的是,咱們不須要在每一次使用閉包以前再繁瑣的寫上 __weak typeof(self) weakSelf = self; 了,取而代之的是捕獲列表的概念:async

@objc private func buttonClick() { 
    thirdViewController.closure = { [weak self] in 
        self?.test()
    }
    navigationController?.pushViewController(thirdViewController, animated: true)
}
複製代碼

再重複一次上面的流程,能夠看到控制檯多了兩條語句:ide

調用 test 方法
SecondViewController-被釋放了
ThirdViewController-被釋放了
複製代碼

只要在捕獲列表中聲明瞭你想要用弱引用的方式捕獲的對象,就能夠及時的規避由閉包致使的循環引用了。可是同時能夠看到,閉包中對於方法的調用從常規的 self.test() 變爲了可選鏈的 self?.test()。這是由於假設閉包在子線程中執行,執行過程當中 self 在主線程隨時有可能被釋放。因爲 self 在閉包中成爲了一個弱引用,所以會自動變爲 nil。在 Swift 中,可選類型的概念讓咱們只能以可選鏈的方式來調用 test。下面修改一下 ThirdViewController 中的代碼:測試

@objc private func buttonClick() {
    // 模擬網絡請求
    DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 5) {
        self.closure?()
    }
}
複製代碼

再次執行相同的操做步驟,此次咱們發現 test 方法沒能正確的獲得調用:優化

SecondViewController-被釋放了
ThirdViewController-被釋放了
複製代碼

在實際的項目中,這可能會致使一些問題,閉包中捕獲的 selfweak 的,有可能在閉包執行的過程當中就被釋放了,致使閉包中的一部分方法被執行了而一部分沒有,應用的狀態所以變得不一致。因而這個時候就要用到 Weak-Strong Dance 了。ui

既然知道了 self 在閉包中成爲了可選類型,那麼除了可選鏈,還可使用可選綁定來處理可選類型:

@objc private func buttonClick() { 
    thirdViewController.closure = { [weak self] in 
        if let strongSelf = self {
            strongSelf.test()
        } else {
            // 處理 self 被釋放時的狀況。
        }
    }
    navigationController?.pushViewController(thirdViewController, animated: true)
}
複製代碼

但這樣老是會讓咱們在閉包中的代碼多出兩句甚至更多,因而還有更優雅的方法,就是使用 guard 語句:

@objc private func buttonClick() { 
    thirdViewController.closure = { [weak self] in 
        guard let strongSelf = self else { return } 
        strongSelf.test()
    }
    navigationController?.pushViewController(thirdViewController, animated: true)
}
複製代碼

一句代碼搞定~

固然,有人看到這裏會說,每次都要使用 strongSelf 來調用 self 的方法,好煩啊……那麼這一點仍是能夠進一步被優化的,SwiftObjective-C 不一樣,是可使用部分關鍵字來聲明變量的,因而咱們能夠:

@objc private func buttonClick() { 
    thirdViewController.closure = { [weak self] in 
        guard let `self` = self else { return } 
        self.test()
    }
    navigationController?.pushViewController(thirdViewController, animated: true)
}
複製代碼

這樣就能夠避免每次書寫 strongSelf 的煩躁感了~

原文地址:Weak-Strong Dance In Swift——如何在 Swift 中優雅的處理閉包致使的循環引用

若是以爲我寫的還不錯,請關注個人微博@小橘爺,最新文章即時推送~

相關文章
相關標籤/搜索