? ? ? ? Swift使用自動引用計數(ARC
)來管理應用程序的內存使用,但ARC
并不是絕對安全的。航哥之前也寫過一篇關于 Swift
內存泄漏原因以及解決辦法的文章(點擊查看)
? ? ? 這次我專門講講在使用 RxSwift
時,容易出現內存泄漏的地方以及解決方法。
一、準備工作
1,頁面創建
(1)這里我準備兩個簡單的頁面:主頁面(ViewController.swift
)和詳情頁(DetailViewController.swift
)
(2)點擊主頁面的“跳轉”按鈕,則會打開詳情頁。
(3)點擊詳情頁左上角的返回按鈕,則詳情頁關閉(頁面被釋放),回到主頁面。
2,頁面代碼
詳情頁代碼很簡單,主要是在反初始化方法(deinit
)中輸出一些信息,方便我們觀察釋放情況。
import UIKit
class DetailViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
deinit {
print(#file, #function)
}
}
3,測試一下
從主頁面跳轉到詳情頁再跳轉回來。可以看到 DetailViewController
的 deinit
方法被調用,說明頁面被成功釋放。
二、一個內存泄漏的樣例
? ? ? ? 使用 RxSwift
時通常都是因為閉包引起的循環強引用而造成內存泄漏。
1,樣例代碼
這里我在詳情頁(DetailViewController.swift
)里增加些功能:
- 當輸入框輸入內容改變時,下方的文本標簽會顯示同樣的文字,而且這些文字還會同步輸出到控制臺中。
- 為了方便觀察,文字顯示我加了個延時。也就是說輸入框輸入后要過個 4 秒鐘,才會顯示到文本標簽上。
import UIKit
import RxSwift
import RxCocoa
class DetailViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
textField.rx.text.orEmpty.asDriver().drive(onNext: {
text in
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
print("當前輸入內容:\(String(describing: text))")
self.label.text = text
}
}).disposed(by: disposeBag)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
deinit {
print(#file, #function)
}
}
2,測試一下
打開詳情頁輸入 1 后立刻返回主頁面。可以看到控制臺過個 4 秒仍然會輸出內容,且 deinit
方法沒有被調用,說明頁面未被釋放。
三、內存泄漏的解決
1,[weak self] 與 [unowned self] 介紹
? ? ? ? 我們只需將閉包捕獲列表定義為弱引用(weak
)、或者無主引用(unowned
)即可解決問題,這二者的使用場景分別如下:
- 如果捕獲(比如
self
)可以被設置為nil
,也就是說它可能在閉包前被銷毀,那么就要將捕獲定義為weak
。 - 如果它們一直是相互引用,即同時銷毀的,那么就可以將捕獲定義為
unowned
。
2,[weak self] 樣例
(1)這里我對上面的樣例代碼稍作修改,增加個 [weak self]
:
import UIKit
import RxSwift
import RxCocoa
class DetailViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
textField.rx.text.orEmpty.asDriver().drive(onNext: {
[weak self] text in
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
print("當前輸入內容:\(String(describing: text))")
self?.label.text = text
}
}).disposed(by: disposeBag)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
deinit {
print(#file, #function)
}
}
(2)仍然按上面的操作步驟測試一下,看到 deinit
方法成功被調用,說明頁面被釋放。
3,[unowned self] 樣例
(1)如果我們不用 [weak self]
而改用 [unowned self]
,返回主頁面 4 秒鐘后由于詳情頁早已被銷毀,這時訪問 label
將會導致異常拋出。
import UIKit
import RxSwift
import RxCocoa
class DetailViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
textField.rx.text.orEmpty.asDriver().drive(onNext: {
[unowned self] text in
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
print("當前輸入內容:\(String(describing: text))")
self.label.text = text
}
}).disposed(by: disposeBag)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
deinit {
print(#file, #function)
}
}
(2)當然如果我們把延時去掉的話,使用 [unowned self]
是完全沒有問題的。
import UIKit
import RxSwift
import RxCocoa
class DetailViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
textField.rx.text.orEmpty.asDriver().drive(onNext: {
[unowned self] text in
print("當前輸入內容:\(String(describing: text))")
self.label.text = text
}).disposed(by: disposeBag)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
deinit {
print(#file, #function)
}
}