Swift中如何在閉包中在對self進行強應用防止閉包中的延時操作獲取不到self
Weak-Strong Dance In Swift——如何在 Swift 中優雅的處理閉包導致的迴圈引用
Objective-C 作為一門資歷很老的語言,添加了 Block 這個特性後深受廣大 iOS 開發者的喜愛。在 Swift 中,對應的概念叫做 Closure,即閉包。雖然更換了名字,但是概念和用法還是相似的,就算是副作用也一樣,有可能導致迴圈引用。
下面我們用一個例子看一下,首先我們需要第一個控制器(FirstViewController
),它所做的就是簡單的推出第二個控制器(SecondViewController
)。
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
方法,用來列印一條語句,看該控制器是否被釋放了。
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;
了,取而代之的是捕獲列表的概念:
@objc private func buttonClick() {
thirdViewController.closure = { [weak self] in
self?.test()
}
navigationController?.pushViewController(thirdViewController, animated: true)
}
再重複一次上面的流程,可以看到控制檯多了兩條語句:
呼叫 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-被釋放了
在實際的專案中,這可能會導致一些問題,閉包中捕獲的 self
是 weak
的,有可能在閉包執行的過程中就被釋放了,導致閉包中的一部分方法被執行了而一部分沒有,應用的狀態因此變得不一致。於是這個時候就要用到 Weak-Strong Dance
了。
既然知道了 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
的方法,好煩啊……那麼這一點還是可以進一步被優化的,Swift
與 Objective-C
不同,是可以使用部分關鍵字來宣告變數的,於是我們可以:
@objc private func buttonClick() {
thirdViewController.closure = { [weak self] in
guard let `self` = self else { return }
self.test()
}
navigationController?.pushViewController(thirdViewController, animated: true)
}
這樣就可以避免每次書寫 strongSelf
的煩躁感了~