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 也继承自它。

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

相关推荐
2501_915921432 分钟前
分析 iOS 描述文件创建与管理中常见的问题
android·ios·小程序·https·uni-app·iphone·webview
东坡肘子14 分钟前
Swift、SwiftUI 与 SwiftData:走向成熟的 2025 -- 肘子的 Swift 周报 #116
人工智能·swiftui·swift
大熊猫侯佩12 小时前
Swift 6.2 列传(第十三篇):香香公主的“倾城之恋”与优先级飞升
swift·编程语言·apple
专业开发者17 小时前
调试 iOS 蓝牙应用的新方法
物联网·macos·ios·cocoa
1024小神19 小时前
Swift配置WKwebview加载网站或静态资源后,开启调试在电脑上debug
swift
tangbin5830851 天前
iOS Swift 可选值(Optional)详解
前端·ios
卷心菜加农炮1 天前
基于Python的FastAPI后端开发框架如何使用PyInstaller 进行打包与部署
ios
北极象2 天前
千问大模型接入示例
ios·iphone·qwen
ipad协议开发2 天前
企业微信 iPad 协议应用机器人开发
ios·企业微信·ipad
kkoral2 天前
基于MS-Swift 为 Qwen3-0.6B-Base 模型搭建可直接调用的 API 服务
python·conda·fastapi·swift