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)
}
}