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

SwiftUI + Firebase社交应用内存溢出问题求助

解决Firebase社交应用内存持续升高问题

问题背景

开发基于Firebase后端的社交应用时,遇到内存持续上涨的问题:每加载一个帖子单元格,内存就逐步增加,最终导致应用无法使用。Firebase的Post结构体包含存储图片链接及其他字段,加载后无法释放。尝试在视图消失时清空帖子数组无效,帖子仍处于加载等待填充状态,希望找到让滚动视图卸载已浏览帖子或其他解决办法。

相关FeedView代码:

struct FeedView: View {
    @StateObject var viewModel = FeedViewModel()
    @State var feedType: FeedType = FeedType.following
    @State var isEmpty: Bool = false
    @State var color: String
    
    var body: some View {
        NavigationStack {
            ScrollView {
                if((viewModel.posts.isEmpty || viewModel.isEmpty) && feedType == FeedType.following) {
                    Image(systemName: "person.2.slash")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .cornerRadius(20.0)
                        .frame(width: 200, height: 200)
                        .foregroundColor(Color.init(hex: color))
                        .padding(.top, 100)
                    Text(viewModel.isEmpty ? "You aren't following any other users..." :"The users your following have no outfits...")
                        .font(.subheadline)
                        .foregroundColor(Color(.systemGray))
                    Text(viewModel.isEmpty ? "Click over a tab to find friends!" : "Check out whats Hot!")
                        .font(.subheadline)
                        .foregroundColor(Color(.systemGray))
                    
                } else {
                    LazyVStack(spacing: 32) {
                        ForEach(viewModel.posts) { post in
                            PostCell(viewModel: PostCellViewModel(post: post), isProfile: false)//, color: color)
                        }
                    }
                    .padding(.top)
                }
            }
            .toolbar(content: {
                ToolbarItem(placement: .navigationBarLeading) {
                    Image("logon")
                        .resizable()
                        .frame(width: 40, height: 40)
                        .padding(.bottom, 40)
                        .padding(.trailing, 20)
                        .padding(.top, 20)
                }
        
                ToolbarItem(placement: .navigation) {
                    Button {
                        feedType = FeedType.following
                        Task{ try await viewModel.fetchPostsFromFollowedUsers() }
                    } label: {
                        Text("Following")
                            .font(.callout)
                            .fontWeight(.semibold)
                            .foregroundColor(feedType == FeedType.following ? Color.init(hex: color) : Color(.systemGray))
                    }
                }
                
                ToolbarItem(placement: .navigation) {
                    Button {
                        feedType = FeedType.hot
                        Task{ try await viewModel.fetchAllPosts() }
                    } label: {
                        Text("Hot")
                            .font(.callout)
                            .fontWeight(.semibold)
                            .foregroundColor(feedType == FeedType.hot ? Color.init(hex: color) : Color(.systemGray))
                    }
                }
                
                ToolbarItem(placement: .navigation) {
                    Button {
                        feedType = FeedType.creator
                        Task { try await viewModel.fetchCreatorPosts() }
                    } label: {
                        Text("Creators")
                            .font(.callout)
                            .fontWeight(.semibold)
                            .foregroundColor(feedType == FeedType.creator ? Color.init(hex: color) : Color(.systemGray))
                    }
                }
            })
            .navigationBarTitleDisplayMode(.inline)
            .refreshable {
                Task {
                    switch(feedType) {
                    case(.following) :
                        Task { try await viewModel.fetchPostsFromFollowedUsers() }
                    case(.hot) :
                        Task { try await viewModel.fetchAllPosts() }
                    case(.creator) :
                        Task { try await viewModel.fetchCreatorPosts() }
                    }
                }
            }
            .navigationDestination(for: User.self) { user in
                ProfileView(user: user)
                    .navigationBarBackButtonHidden(true)
            }
            .navigationDestination(for: Post.self) { post in
                PostCell(viewModel: PostCellViewModel(post: post), isProfile: true)//, color: "")
                    .navigationBarBackButtonHidden(true)
            }
            .navigationDestination(for: SearchViewModelConfig.self) { config in
                ListView(config: config, color: .blue)
                    .navigationBarBackButtonHidden(true)
            }
        }
    }
}

核心解决思路

1. 修正单元格ViewModel的生命周期

你已经用到LazyVStack,但单元格里的ViewModel如果用@StateObject会被强持有,无法随单元格回收。把PostCell里的ViewModel声明改成@ObservedObject:

struct PostCell: View {
    @ObservedObject var viewModel: PostCellViewModel // 替换原@StateObject
    // ... 其他代码
}

2. 优化图片加载与回收

图片是内存占用核心,必须做好资源管理:

  • 优先用成熟的图片加载库(比如KingfisherSwiftUI),这类库会自动处理缓存、内存释放和请求取消。
  • 如果自定义图片加载逻辑,在PostCell的onDisappear里取消加载任务并释放资源:
PostCell(viewModel: viewModel, isProfile: false)
    .onDisappear {
        viewModel.cancelImageLoadTasks() // 需在ViewModel中实现取消逻辑
    }

3. 清理Firebase监听与强引用

检查ViewModel中的Firebase监听,避免泄漏:

  • 保存ListenerRegistration实例,在ViewModel销毁时移除监听:
class FeedViewModel: ObservableObject {
    private var firestoreListeners: [ListenerRegistration] = []
    
    func fetchPostsFromFollowedUsers() async throws {
        let listener = Firestore.firestore().collection("posts")
            .whereField("userId", in: followedUserIds)
            .addSnapshotListener { [weak self] snapshot, error in
                // 处理数据逻辑
            }
        firestoreListeners.append(listener)
    }
    
    deinit {
        firestoreListeners.forEach { $0.remove() }
    }
}
  • 闭包中必须用[weak self]避免循环引用,防止ViewModel无法被释放。

4. 切换Feed时先清空旧数据

切换Following/Hot/Creators标签时,先清空旧帖子数组,避免新旧数据同时占用内存:

Button {
    feedType = FeedType.hot
    viewModel.posts.removeAll() // 先清空旧数据
    Task{ try await viewModel.fetchAllPosts() }
} label: {
    Text("Hot")
        .font(.callout)
        .fontWeight(.semibold)
        .foregroundColor(feedType == FeedType.hot ? Color.init(hex: color) : Color(.systemGray))
}

(Following和Creators标签做同样修改)

5. 用工具定位泄漏

打开Xcode的Memory Graph Debugger,滚动加载一批帖子后切换标签,查看Post、PostCellViewModel或图片对象是否未被释放,快速定位循环引用或强持有问题。

内容的提问来源于stack exchange,提问作者Tyler George

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 18:35:09