从Firebase/缓存获取帖子时主线程阻塞,求代码问题排查
问题排查与修复建议
问题现象
无论从缓存还是Firebase获取帖子,应用都会冻结,主线程阻塞,加载大量帖子时问题尤为明显,调整缓存线程配置后仍未解决。
待排查代码
func fetchPosts(completion: @escaping (Result<[PostWithUser], Error>) -> Void) { DispatchQueue.global(qos: .background).async { [weak self] in guard let self = self else { return } if !self.cachedPosts.isEmpty { DispatchQueue.main.async { completion(.success(self.cachedPosts)) } return } self.postsRef.order(by: "timestamp", descending: true).getDocuments { snapshot, error in if let error = error { DispatchQueue.main.async { completion(.failure(error)) } return } guard let documents = snapshot?.documents else { DispatchQueue.main.async { completion(.success([])) } return } var userIds = Set<String>() var posts: [Post] = [] for document in documents { let data = document.data() guard let userId = data["userId"] as? String, let parentId = data["parentId"] as? String, let groupId = data["groupId"] as? String, let text = data["text"] as? String, let isPinned = data["isPinned"] as? Bool, let imageUrl = data["imageUrl"] as? String, let videoUrl = data["videoUrl"] as? String, let timestamp = data["timestamp"] as? Timestamp, let views = data["views"] as? Int else { continue } let post = Post(id: document.documentID, userId: userId, parentId: parentId, groupId: groupId, text: text, imageUrl: imageUrl, videoUrl: videoUrl, timestamp: timestamp.dateValue(), isPinned: isPinned, likedBy: [], views: views) posts.append(post) userIds.insert(userId) } self.fetchUsers(withIds: Array(userIds)) { result in switch result { case .success(let users): let usersDict = Dictionary(uniqueKeysWithValues: users.map { ($0.id, $0) }) let postsWithUsers = posts.compactMap { post -> PostWithUser? in guard let user = usersDict[post.userId] else { return nil } return PostWithUser(post: post, user: user) } DispatchQueue.main.async { self.cachedPosts = postsWithUsers completion(.success(postsWithUsers)) } case .failure(let error): DispatchQueue.main.async { completion(.failure(error)) } } } } } } private func fetchUsers(withIds ids: [String], completion: @escaping (Result<[User], Error>) -> Void) { let group = DispatchGroup() var users: [User] = [] var fetchError: Error? for id in ids { group.enter() self.usersRef.document(id).getDocument { document, error in defer { group.leave() } if let error = error { fetchError = error return } if let document = document, let data = document.data() { let user = User( id: id, username: data["username"] as? String ?? "", bio: data["bio"] as? String ?? "", profilePictureUrl: data["profileImageUrl"] as? String ?? "", privateProfile: data["privateProfile"] as? Bool ?? false, privateFollowerList: data["privateFollowerList"] as? Bool ?? false, privateFollowingList: data["privateFollowingList"] as? Bool ?? false, privateReplies: data["privateReplies"] as? Bool ?? false, privateLikes: data["privateLikes"] as? Bool ?? false, accountDeactivated: data["accountDeactivated"] as? Bool ?? false ) users.append(user) } } } group.notify(queue: .global()) { if let error = fetchError { completion(.failure(error)) } else { completion(.success(users)) } } }
阻塞原因分析
缓存的线程安全问题
cachedPosts是直接在后台线程读取、主线程写入的非线程安全数组,存在竞态条件。当主线程和后台线程同时操作数组时,可能触发内部锁等待甚至数组崩溃,直接导致主线程阻塞。批量用户请求的主线程占用
Firebase iOS SDK默认会将所有回调派发到主线程执行。fetchUsers中循环逐个请求用户文档,当用户ID数量较多时,大量回调任务会在主线程堆积,每个回调都包含数据解析和数组插入操作,直接挤占主线程的UI渲染资源,引发冻结。低效的用户查询方式
逐个请求用户文档会产生大量网络请求,不仅增加网络开销,还会导致更多主线程回调任务,进一步加剧主线程阻塞。
修复方案
1. 保证缓存的线程安全
用串行队列封装cachedPosts的读写操作,避免竞态条件:
private let cachedPostsQueue = DispatchQueue(label: "com.yourapp.cachedPosts") private var _cachedPosts: [PostWithUser] = [] var cachedPosts: [PostWithUser] { get { cachedPostsQueue.sync { _cachedPosts } } set { cachedPostsQueue.sync { _cachedPosts = newValue } } }
2. 批量查询用户,减少请求数
替换逐个请求为Firebase批量查询,大幅减少网络请求和回调次数:
private func fetchUsers(withIds ids: [String], completion: @escaping (Result<[User], Error>) -> Void) { guard !ids.isEmpty else { completion(.success([])) return } // 用whereField批量查询用户 usersRef.whereField("id", in: ids).getDocuments(queue: DispatchQueue.global()) { snapshot, error in if let error = error { completion(.failure(error)) return } guard let documents = snapshot?.documents else { completion(.success([])) return } let users = documents.compactMap { document -> User? in let data = document.data() return User( id: document.documentID, username: data["username"] as? String ?? "", bio: data["bio"] as? String ?? "", profilePictureUrl: data["profileImageUrl"] as? String ?? "", privateProfile: data["privateProfile"] as? Bool ?? false, privateFollowerList: data["privateFollowerList"] as? Bool ?? false, privateFollowingList: data["privateFollowingList"] as? Bool ?? false, privateReplies: data["privateReplies"] as? Bool ?? false, privateLikes: data["privateLikes"] as? Bool ?? false, accountDeactivated: data["accountDeactivated"] as? Bool ?? false ) } completion(.success(users)) } }
3. 指定Firebase回调到后台线程
在所有Firebase查询中指定queue参数为后台队列,避免回调占用主线程:
// 在fetchPosts中修改getDocuments调用 self.postsRef.order(by: "timestamp", descending: true).getDocuments(queue: DispatchQueue.global()) { snapshot, error in // 原有逻辑不变 }
4. 优化数据解析的内存效率
如果帖子数量极大,可将数组posts的初始化改为预分配容量,减少内存重新分配的开销:
var posts: [Post] = [] posts.reserveCapacity(documents.count) // 预分配容量
内容的提问来源于stack exchange,提问作者CobraCodes
相关产品推荐
相关产品推荐

