Swift - RxSwift的使用详解42([unowned self] 与 [weak self])
这次我专门讲讲在使用 RxSwift 时,容易出现内存泄漏的地方以及解决方法。
一、准备工作
1,页面创建
(1)这里我准备两个简单的页面:主页面(ViewController.swift)和详情页(DetailViewController.swift)
(2)点击主页面的“跳转”按钮,则会打开详情页。
(3)点击详情页左上角的返回按钮,则详情页关闭(页面被释放),回到主页面。
(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) } }