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
的煩躁感了~
原文地址:Weak-Strong Dance In Swift——如何在 Swift 中優雅的處理閉包導致的迴圈引用
如果覺得我寫的還不錯,請關注我的微博@小橘爺,最新文章即時推送~