RxCocoa绑定TableView滚动时内存占用过高问题排查
问题核心原因
这个问题和RxCocoa的TableView绑定逻辑没有关系,核心问题出在你的数据源设计上:
你在DealModel中直接强持有了全量解码后的UIImage实例,而BehaviorRelay会持续持有整个数据源数组不释放,等于所有已经加载过的图片都会常驻内存。
UITableView的Cell复用机制只会回收出屏的Cell对象,不会干预你自己持有的数据源内存——当你向下滚动时,每加载一个从未显示过的Cell,就会把对应位置的UIImage从数据源取出来渲染,滚过的行越多,常驻内存的UIImage就越多,直到所有行都滚过一遍,内存达到峰值就不再上涨,和你描述的现象完全吻合。
按常规图片尺寸估算:单张全屏分辨率的图片解码后占用内存在10-20MB,只要100张左右的图就能冲到1.7GB内存,完全符合你遇到的情况。
另外你当前的Firebase回调逻辑还有额外的性能问题:每遍历一条文档就调用一次userDealsRelay.accept,会触发TableView多次无意义的全量刷新,虽然不是内存暴涨的核心诱因,但会大幅增加主线程开销。
排查方向
- 打开Xcode Memory Graph,筛选内存中存活的
UIImage实例,统计数量如果和数据源中DealModel的总数一致,即可确认问题 - 检查下载的图片是否是未压缩的原图:如果服务端返回的是2000px以上分辨率的原图,即使你在Cell里把它缩放到几十像素的大小显示,解码后的UIImage依然会按原始分辨率占用内存
- 检查Cell复用逻辑中是否存在循环引用、是否在复用时清空了旧图片引用,这类问题一般会导致内存无限上涨,和你当前滚到底就不涨的现象不匹配,可以作为次要排查点
可行解决方案
- 重构数据源,不要在模型中存储UIImage对象
把DealModel中存储UIImage的字段替换为存储图片路径/URL字符串,字符串本身的内存占用可以忽略。图片的加载、缓存、复用清理交给成熟的图片加载库处理(比如Kingfisher、SDWebImage),在Cell的配置方法中直接给图片视图触发加载,这类库会自动处理:- 滚动时异步加载对应位置的图片,不阻塞主线程
- Cell复用时自动取消未完成的下载任务,清空旧图片避免错位
- 自带按系统内存情况自动淘汰的内存缓存,不会把所有图片永久常驻
代码修改示例:
// 修改DealModel定义 struct DealModel: Hashable { var storeID: String? var dealImagePath: String? // 替换原有的var dealImage: UIImage?字段 var dealTitle: String? var dealDesc: String? var dealID : String? var storeTitle: String? var sender: String? var userName: String? var distance: Double? var senderUID: String? // 初始化方法对应修改参数即可 } // Cell配置方法中修改图片加载逻辑 func configureProfile(dataModel: DealModel) { // 其余文本等配置逻辑保持不变 guard let path = dataModel.dealImagePath, let imgURL = URL(string: path) else { dealImageView.image = nil return } // 以Kingfisher为例,SDWebImage用法类似 dealImageView.kf.setImage(with: imgURL) } - 修正Firebase回调的重复刷新问题
不要在遍历文档的for循环中每处理一条数据就调用一次accept,等所有文档解析完成、模型构造完毕后,统一在主线程调用一次accept刷新列表即可,避免无意义的多次UI刷新。
代码修改示例:db.collectionGroup("deals").whereField("Sender", isEqualTo: senderEmail!).addSnapshotListener { (snapshot, error) in guard error == nil else { print("error?.localizedDescription collectiongroup = \(error?.localizedDescription)") return } self.dealNumber = snapshot?.documents.count ?? 0 DispatchQueue.main.async { self.dealsPosted.attributedText = NSMutableAttributedString() .bold("\(self.dealNumber)\n") .normal("Deals") } var parsedDeals: [DealModel] = [] for document in snapshot!.documents { let data = document.data() guard let sender = data["Sender"] as? String, let imagePath = data["ImagePath"] as? String, let dealTitle = data["DealTitle"] as? String, let dealDesc = data["DealDesc"] as? String, let storeID = data["StoreID"] as? String, let storeTitle = data["StoreTitle"] as? String, let dealID = data["DealID"] as? String, let userName = data["UserName"] as? String, let distance = data["Distance"] as? Double, let senderUID = data["SenderUID"] as? String else { continue } // 构造模型时只存图片路径,不要提前下载图片转UIImage存在模型里 let deal = DealModel(storeID: storeID, dealImagePath: imagePath, dealTitle: dealTitle, dealDesc: dealDesc, dealID: dealID, storeTitle: storeTitle, sender: sender, userName: userName, distance: distance, senderUID: senderUID) parsedDeals.append(deal) } // 所有数据处理完成后统一刷新一次 DispatchQueue.main.async { userDealsRelay.accept(parsedDeals.removingDuplicates()) } } - 如果不打算引入第三方图片库,自行实现加载逻辑时必须满足三个要求:
- 图片下载完成后按照Cell内图片视图的实际尺寸做缩略图解码,不要直接把全尺寸原图赋值给UIImageView
- 用
NSCache实现内存缓存,设置合理的总内存占用上限,系统内存不足时自动释放缓存内容,绝对不要把所有图片强引用在数据源数组里 - Cell进入复用流程时,主动清空当前图片、取消正在执行的对应下载任务,避免图片错位和无效内存占用
内容的提问来源于stack exchange,提问作者iPancake
相关产品推荐
相关产品推荐

