使用Firestore监听器时出现内存泄漏(是否为误报?)
iOS Firestore群聊应用内存泄漏问题
问题现象
开发的iOS应用从Firestore获取并监听群聊内容,仅在以下场景出现内存泄漏:
- 启用过滤条件
if !message.isSaved || message.hasBlocked || message.isBlocked,且存在至少一个Firestore监听器时; - 移除全部过滤条件后,泄漏完全消失。
目前怀疑是Instrument误报,因为应用内存从未超过200MB,运行状态稳定,附带Instrument泄漏检测结果截图。
相关代码
GroupchatVC代码
private var lastDocumentSnapshot: DocumentSnapshot? private var listeners: [ListenerRegistration] = [] // 修正变量名,统一管理监听器 private var isListening = true private var theyBlockedU: [String] = [] private var uBlockedThem: [String] = [] private var groupsDictionary: [String: Group] = [:] // 补充原代码未声明的变量 private var groups = [Group]() override func viewDidLoad() { super.viewDidLoad() fetchGroups() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.isListening = true } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.isListening = false // 统一移除所有监听器,释放内存 listeners.forEach { $0.remove() } listeners.removeAll() } // MARK: - API func fetchGroups() { self.fetchGroups() { [weak self] groups in guard let self = self else{return} groups.forEach { group in self.groupsDictionary[group.recentMessage.groupID] = group } self.groups = Array(self.groupsDictionary.values) self.groups.sort(by: { $0.recentMessage.timestamp.compare($1.recentMessage.timestamp) == .orderedDescending }) self.tableView.reloadData() } self.tableView.refreshControl?.endRefreshing() } func fetchGroups(completion: @escaping([Group]) -> Void) { var query: Query! tableView.refreshControl?.beginRefreshing() if groups.isEmpty { query = COLLECTION_GROUPS.order(by: "timestamp", descending: false) } else { guard let lastDocSnapshot = lastDocumentSnapshot else { print("Error: lastDocumentSnapshot is nil") return } query = COLLECTION_GROUPS.order(by: "timestamp", descending: false).end(beforeDocument: lastDocSnapshot) } let listener = query.addSnapshotListener { [weak self] (snapshot, err) in guard let self = self else{return} if let err = err { print("\(err.localizedDescription)") return } guard let snapshot = snapshot else { return } if snapshot.isEmpty { self.isLoading = false self.stopActivityIndicator() self.tableView.refreshControl?.endRefreshing() return } guard let lastSnap = snapshot.documents.first else { return } self.lastDocumentSnapshot = lastSnap snapshot.documentChanges.forEach({ (change) in let dictionary = change.document.data() var message = GroupMessage(dictionary: dictionary) UserService.fetchUser(withUid: message.senderID) { [weak self] user in guard let self = self else { return } self.isUserBlocked(ownerID: message.ownerID) { isBlocked in message.isBlocked = isBlocked self.isBlockedByUser(ownerID: message.ownerID) { hasBlocked in message.hasBlocked = hasBlocked self.listenForGroupSaved(groupID: message.groupID) { isSaved in guard let self = self else { return } message.isSaved = isSaved let group = Group(user: user, recentMessage: message) if !message.isSaved || message.hasBlocked || message.isBlocked { self.groups = self.groups.filter { $0.recentMessage.groupID != message.groupID } self.tableView.reloadData() return } else { if let index = self.groups.firstIndex(where: { $0.recentMessage.groupID == message.groupID }) { self.groups[index] = group } else { self.groups.append(group) } self.groups.sort(by: { $0.recentMessage.timestamp.compare($1.recentMessage.timestamp) == .orderedDescending }) self.tableView.reloadData() } } } } }) }) self.tableView.refreshControl?.endRefreshing() self.lastDocumentSnapshot = snapshot.documents.first } listeners.append(listener) // 将监听器加入统一管理数组 } func listenForGroupSaved(groupID: String, completion: @escaping (Bool) -> Void) { guard let currentUser = Auth.auth().currentUser else {return} let docRef = COLLECTION_USERS.document(currentUser.uid).collection("isGroupSaved").document(groupID) let listener = docRef.addSnapshotListener { (snapshot, error) in if let error = error { print("Error listening for savedGroup changes: \(error.localizedDescription)") return } let isSaved = snapshot?.data()?["isSaved"] as? Bool ?? false completion(isSaved) } listeners.append(listener) } func isUserBlocked(ownerID: String, completion: @escaping(Bool) -> Void) { guard let currentUser = Auth.auth().currentUser else { return } let listener = COLLECTION_USERS.document(currentUser.uid).collection("blocked").addSnapshotListener { [weak self] (querySnapshot, err) in guard let self = self else { return } if let err = err { print("Error getting documents: \(err)") return } self.uBlockedThem = querySnapshot!.documents.map({ $0.documentID }) let isBlocked = self.uBlockedThem.contains(ownerID) completion(isBlocked) } listeners.append(listener) } func isBlockedByUser(ownerID: String, completion: @escaping(Bool) -> Void) { guard let currentUser = Auth.auth().currentUser else{return} let listener = COLLECTION_USERS.document(currentUser.uid).collection("isBlockedBy").addSnapshotListener { [weak self] (querySnapshot, err) in guard let self = self else { return } if let err = err { print("Error getting documents: \(err)") return } self.theyBlockedU = querySnapshot!.documents.map({ $0.documentID }) let isBlocked = self.theyBlockedU.contains(ownerID) completion(isBlocked) } listeners.append(listener) }
Group结构体代码
struct Group { var user: User var recentMessage: GroupMessage } struct GroupMessage { var user: User? var text: String var senderID: String var groupID : String var ownerID: String var timestamp: Timestamp var isSaved: Bool var isBlocked: Bool var hasBlocked: Bool init(dictionary: [String: Any]) { self.text = dictionary["text"] as? String ?? "" self.senderID = dictionary["senderID"] as? String ?? "" self.groupID = dictionary["groupID"] as? String ?? "" self.ownerID = dictionary["ownerID"] as? String ?? "" self.timestamp = dictionary["timestamp"] as? Timestamp ?? Timestamp(date: Date()) self.isSaved = dictionary["isSaved"] as? Bool ?? false self.isBlocked = dictionary["isBlocked"] as? Bool ?? false self.hasBlocked = dictionary["hasBlocked"] as? Bool ?? false } }
问题分析与修复方案
核心问题
- 监听器未统一管理:原代码中所有Firestore监听器都是局部变量,未存入类级别的管理数组,导致VC消失时无法彻底移除监听器。这些监听器会持续持有VC引用,即使VC已不可见,内存也无法释放,尤其在过滤条件触发提前return时,关联监听器仍在后台运行。
- 过滤条件触发时未清理资源:满足过滤条件执行
return时,之前创建的listenForGroupSaved、isUserBlocked等监听器未被移除,持续占用内存并持有VC引用。
修复步骤
- 统一管理监听器:将所有创建的
ListenerRegistration实例加入listeners数组,在viewDidDisappear时统一遍历移除并清空数组,确保VC能被正确释放。 - 优化闭包逻辑:减少嵌套闭包层级,或在过滤条件触发时,单独移除当前消息关联的监听器(比如用字典存储groupID对应的监听器,过滤时移除对应条目)。
- 补全代码遗漏:添加原代码中用到但未声明的
groupsDictionary变量,避免运行时错误。
额外建议
- 即使当前内存稳定,也建议修复监听器管理问题,避免长期运行后内存累积引发性能下降。
- 使用Xcode Memory Graph Debugger查看泄漏对象的引用链,确认监听器是否为泄漏根源。
内容的提问来源于stack exchange,提问作者Unknown
相关产品推荐
相关产品推荐

