前言
在RxSwift框架中,我们往往会在序列订阅的后面做disposed操作:
swift
button.rx.tap
.subscribe(onNext: {
print("button Tapped")
})
.disposed(by: disposeBag)
.disposed(by: disposeBag)
的目的是将当前的RxSwift订阅(Disposable)交给disposeBag统一管理,当disposeBag被释放或重置时,所有加入其中的订阅会自动被释放(即取消订阅),从而避免内存泄漏和重复事件响应。
简而言之:
它让订阅的生命周期和disposeBag一致,自动管理资源释放,无需手动调用dispose()。
不过错误的用法,可能会让你的代码产生意想不到的bug。
Bug起因
在UITableViewCell构建的cell中有button,button的点击事件通过RxRelay将其在Controller层进行出发,代码大致写这个样子:
swift
class ViewController: UIViewController {
let disposeBag = DisposeBag()
func binding() {
viewModel.outputs.dataSource
.asDriver(onErrorJustReturn: [])
.drive(tableView.rx.items) { (tableView, _, info) in
let cell = tableView.dequeueReusableCell(withIdentifier: InfoViewCell.className) as! InfoViewCell
cell.info = info
cell.buttonTap.subscribe(onNext: { [weak self] model in
guard let self else { return }
let vc = SingleTabListController(type: self.type, tabModel: model)
self.navigationController?.pushViewController(vc, animated: true)
}).disposed(by: disposeBag)
return cell
}
.disposed(by: disposeBag)
}
}
Bug显现:你会发现点击一次cell,会触发好几次push操作,SingleTabListController页面被推进来好几次。
分析问题与解决
cell的disposed操作,关联的居然是ViewController的disposeBag,想想这波操作就不太合理。此时相当于ViewController的disposeBag被释放或重置时,cell上面的订阅会自动被释放。
ViewController只有在关闭的时候才会将disposeBag释放啊!
那么我们改成这样试试:
swift
func binding() {
viewModel.outputs.dataSource
.asDriver(onErrorJustReturn: [])
.drive(tableView.rx.items) { (tableView, _, info) in
let cell = tableView.dequeueReusableCell(withIdentifier: InfoViewCell.className) as! InfoViewCell
cell.info = info
cell.buttonTap.subscribe(onNext: { [weak self] model in
guard let self else { return }
let vc = SingleTabListController(type: self.type, tabModel: model)
self.navigationController?.pushViewController(vc, animated: true)
/// 注意这里,我改成了使用cell.disposeBag
}).disposed(by: cell.disposeBag)
return cell
}
.disposed(by: disposeBag)
}
在cell的buttonTap中,我使用了.disposed(by: cell.disposeBag)
这样应该可以解决问题吧。
然而,问题并没有得到解决,为什么?
让我们进一步分析问题:
UITableViewCell 会被UITableView复用(即同一个 cell 实例会显示不同的数据),如果不重置 disposeBag,之前 cell 上的订阅还会存在,进而引起持续订阅的问题!
有没有办法消除之前的订阅,进行重新订阅呢?
答案是有的,因为UITableViewCell有一个prepareForReuse()
方法!
swift
class BaseDisposeBagCell: UITableViewCell {
private(set) var disposeBag = DisposeBag()
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
}
在prepareForReuse
方法中重新赋值disposeBag = DisposeBag()
的目的是清除之前 cell 绑定的所有 RxSwift 订阅,防止内存泄漏和数据错乱。
通过在prepareForReuse
中重新创建一个新的DisposeBag
,之前的所有订阅都会被释放,确保 cell 复用时是"干净"的。这样每次 cell 复用时,RxSwift 的绑定都是全新的,符合预期的生命周期管理。
让涉及rx操作的Cell都继承这个BaseDisposeBagCell
即可!
再思考,有没有更为简单的方法解决这个问题呢?
其实当时遇到这个问题的时候,我正在赶项目进度,根本没有时间去思考如何解决rx传递事件带来的问题。
你看的没错,上面的解决方案是在复盘的阶段完成。
当时我们唯一的想法就是解决这个bug。
既然RxSwift当前我解决不了,那么我就回归Swift的方式!
通过在button上面addTarget,然后使用callback或者delegate是完全不会出现这个问题的!
RxSwift这个工具没有啥问题,出现问题的是我当时对disposeBag理解不够!
总结
如果锋利的工具当下无法帮助到你,拿起原始的武器先战斗吧!
技巧
- 每次复用时重置DisposeBag
在prepareForReuse()
方法中重新赋值disposeBag = DisposeBag()
,确保cell复用时释放旧的订阅,避免内存泄漏和数据错乱。 - 绑定数据时使用cell的DisposeBag
在cell的bind
或configure
方法中,所有Rx绑定都用cell自己的DisposeBag管理。
注意事项
- 不要在cell的
init
方法中绑定数据,应该在cell复用时(如cellForRowAt
或自定义bind
方法)绑定。 - 避免在cell外部直接调用
dispose()
,应通过DisposeBag自动管理。 - 保证DisposeBag的生命周期与cell一致,防止订阅泄漏。
扩展
既然UITableViewCell在使用过程中需要注意,那是不是还要其他一些类需要注意呢?
在 UIKit 框架中,有 prepareForReuse()
方法的主要类有以下几个,它们都用于复用机制(如表格、集合视图的 cell 或 view):
-
UITableViewCell
用于 UITableView 的单元格复用。
-
UICollectionViewCell
用于 UICollectionView 的单元格复用。
-
UITableViewHeaderFooterView
用于 UITableView 的 section 头/尾视图复用。
-
UICollectionReusableView
用于 UICollectionView 的 supplementary view(如 header/footer)复用,
UICollectionViewCell
也继承自它。
以上都需要时使用过程中多加小心。