Firebase循环监听多条目时DispatchGroup崩溃问题及解决方案咨询
解决实时信息流中用户数据更新导致DispatchGroup崩溃的问题
问题背景
开发关注对象帖子信息流时,使用实时监听获取帖子列表,再为每个帖子调用observeUser监听作者数据(头像、用户名),用DispatchGroup等待所有作者数据加载完成后刷新TableView。首次运行正常,但当作者更新个人信息时,observeUser的实时回调会再次调用dispatchGroup.leave(),此时没有对应的enter(),导致DispatchGroup计数异常崩溃。需要保留实时更新特性,不能改用observeSingleValue()。
问题根源
原代码中,每次帖子列表的observe(.value)触发(包括帖子更新),都会创建新的DispatchGroup并为每个帖子调用enter()。而observeUser是持续监听,首次回调时调用leave()完成计数,但后续作者信息更新时,回调会再次执行leave(),此时对应帖子的enter()已经被处理过,DispatchGroup计数变为负数,触发崩溃。
解决方案
核心思路是将首次加载的等待逻辑和后续实时更新逻辑解耦,通过缓存帖子和复用用户监听来避免重复调用leave():
1. 新增缓存与监听句柄存储
在类中维护帖子缓存和用户监听的句柄,避免重复解码帖子和重复监听同一用户:
// 缓存已加载的帖子,key为postID(需确保UserPost有postID属性) private var postsCache: [String: UserPost] = [:] // 存储用户监听句柄,key为userID,用于页面销毁时移除监听 private var userObservers: [String: DatabaseHandle] = [:]
2. 修改信息流监听逻辑
重新实现observePosts函数,区分首次加载和后续更新:
func observePosts(for userID: String, completion: @escaping ([UserPost], String) -> Void) { let databaseKey = "Posts/\(userID)/" databaseRef.child(databaseKey).queryOrderedByKey().queryLimited(toLast: 12).observe(.value) { [weak self] snapshot in guard let self = self else { return } var lastSeenPostKey = "" let dispatchGroup = DispatchGroup() var newPostCount = 0 // 遍历快照处理帖子 for snapshotChild in snapshot.children.reversed() { guard let postSnapshot = snapshotChild as? DataSnapshot, let postDictionary = postSnapshot.value as? [String: Any] else { print("Couldn't cast snapshot as DataSnapshot") continue } lastSeenPostKey = postSnapshot.key let postID = postSnapshot.key // 缓存中已有该帖子,直接跳过 if self.postsCache[postID] != nil { continue } // 处理新帖子 newPostCount += 1 dispatchGroup.enter() do { let jsonData = try JSONSerialization.data(withJSONObject: postDictionary) var decodedPost = try JSONDecoder().decode(UserPost.self, from: jsonData) decodedPost.postID = postID // 给帖子绑定唯一ID // 复用用户监听:同一用户只监听一次 if self.userObservers[decodedPost.userID] == nil { let observerHandle = UserService.shared.observeUser(for: decodedPost.userID) { [weak self] updatedAuthor in guard let self = self else { return } // 更新所有该作者的帖子信息 for (key, var post) in self.postsCache where post.userID == decodedPost.userID { post.author = updatedAuthor self.postsCache[key] = post } // 实时刷新UI let sortedPosts = self.postsCache.values.sorted { $0.timestamp > $1.timestamp } completion(sortedPosts, lastSeenPostKey) } self.userObservers[decodedPost.userID] = observerHandle } // 首次获取作者数据,更新缓存并完成DispatchGroup计数 UserService.shared.observeUser(for: decodedPost.userID) { [weak self] initialAuthor in guard let self = self else { return } decodedPost.author = initialAuthor self.postsCache[postID] = decodedPost dispatchGroup.leave() } } catch { print("Couldn't create UserPost from postDictionary") dispatchGroup.leave() // 出错也要手动leave,避免死等 } } // 没有新帖子,直接用缓存数据刷新UI if newPostCount == 0 { let sortedPosts = self.postsCache.values.sorted { $0.timestamp > $1.timestamp } completion(sortedPosts, lastSeenPostKey) return } // 所有新帖子的首次作者数据加载完成后,刷新UI dispatchGroup.notify(queue: .main) { let sortedPosts = self.postsCache.values.sorted { $0.timestamp > $1.timestamp } completion(sortedPosts, lastSeenPostKey) } } }
3. 内存清理(可选但重要)
在页面销毁时移除所有监听,避免内存泄漏:
deinit { // 移除所有用户监听 for handle in userObservers.values { UserService.shared.removeObserver(withHandle: handle) } // 移除帖子列表的监听 databaseRef.removeAllObservers() }
方案优势
- 保留实时更新:作者信息变化时,所有关联帖子会自动更新并触发UI刷新
- 避免DispatchGroup崩溃:仅在首次加载新帖子时使用DispatchGroup,后续更新不再调用
leave() - 性能优化:同一用户只监听一次,帖子重复出现时直接复用缓存数据
内容的提问来源于stack exchange,提问作者adseeAnn
相关产品推荐
相关产品推荐

