You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从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))
        }
    }
}

阻塞原因分析

  1. 缓存的线程安全问题
    cachedPosts是直接在后台线程读取、主线程写入的非线程安全数组,存在竞态条件。当主线程和后台线程同时操作数组时,可能触发内部锁等待甚至数组崩溃,直接导致主线程阻塞。

  2. 批量用户请求的主线程占用
    Firebase iOS SDK默认会将所有回调派发到主线程执行。fetchUsers中循环逐个请求用户文档,当用户ID数量较多时,大量回调任务会在主线程堆积,每个回调都包含数据解析和数组插入操作,直接挤占主线程的UI渲染资源,引发冻结。

  3. 低效的用户查询方式
    逐个请求用户文档会产生大量网络请求,不仅增加网络开销,还会导致更多主线程回调任务,进一步加剧主线程阻塞。

修复方案

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 11:44:55