RxSwift:使用UITableViewCell的注意事项

前言

在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理解不够!

总结

如果锋利的工具当下无法帮助到你,拿起原始的武器先战斗吧!

技巧

  1. 每次复用时重置DisposeBag
    prepareForReuse()方法中重新赋值disposeBag = DisposeBag(),确保cell复用时释放旧的订阅,避免内存泄漏和数据错乱。
  2. 绑定数据时使用cell的DisposeBag
    在cell的bindconfigure方法中,所有Rx绑定都用cell自己的DisposeBag管理。

注意事项

  • 不要在cell的init方法中绑定数据,应该在cell复用时(如cellForRowAt或自定义bind方法)绑定。
  • 避免在cell外部直接调用dispose(),应通过DisposeBag自动管理。
  • 保证DisposeBag的生命周期与cell一致,防止订阅泄漏。

扩展

既然UITableViewCell在使用过程中需要注意,那是不是还要其他一些类需要注意呢?

在 UIKit 框架中,prepareForReuse() 方法的主要类有以下几个,它们都用于复用机制(如表格、集合视图的 cell 或 view):

  1. UITableViewCell

    用于 UITableView 的单元格复用。

  2. UICollectionViewCell

    用于 UICollectionView 的单元格复用。

  3. UITableViewHeaderFooterView

    用于 UITableView 的 section 头/尾视图复用。

  4. UICollectionReusableView

    用于 UICollectionView 的 supplementary view(如 header/footer)复用,UICollectionViewCell 也继承自它。

以上都需要时使用过程中多加小心。

相关推荐
帅次4 小时前
Flutter Container 组件详解
android·flutter·ios·小程序·kotlin·iphone·xcode
SoaringHeart6 小时前
SwiftUI组件封装:仿 Flutter 原生组件 Wrap实现
ios·swiftui
YungFan6 小时前
SwiftUI-自定义与扩展
swiftui·swift
I烟雨云渊T7 小时前
iOS 抖音首页头部滑动标签的实现
ios
十月ooOO8 小时前
uniapp 云打包 iOS 应用上传到 app store 商店的过程
ios·uni-app
帅次8 小时前
Flutter setState() 状态管理详细使用指南
android·flutter·ios·小程序·kotlin·android studio·iphone
东坡肘子9 小时前
WWDC 2025 开发者特辑 | 肘子的 Swift 周报 #088
swiftui·swift·wwdc
kymjs张涛10 小时前
前沿技术周刊 2025-06-03
android·前端·ios