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

Swift中使用Firebase监听器的内存泄漏问题及修复方案

SwiftUI中FeedViewModel实例重复与数据不一致问题解决

问题现象

  • 内存检测显示存在2个FeedViewModel实例,但Firebase中仅创建了1条Feed数据;
  • 在FeedRow初始化断点中打印feedViewModel.text,结果为This is a firebase text 5;但在视图显示文本的断点中打印该值,结果变为This is a firebase text 4。

相关代码

Firebase仓库fetchAllFeeds函数

func fetchAllFeeds(completion: @escaping (Result<[Feed], FirebaseError>) -> Void) {
    self.feedIdsListener = FirebaseManager.shared.firestore.collection("feeds").addSnapshotListener { querySnapshot, error in
        if let error {
            print("Fetch feeds failed: (error)")
            completion(.failure(.unknown(error)))
            return
        }

        guard let documents = querySnapshot?.documents else {
            print("Query Snapshot has no documents")
            completion(.failure(.collectioNotFound))
            return
        }
        
        let allFeeds = documents.compactMap { document in
            try? document.data(as: Feed.self)
        }
        
        completion(.success(allFeeds))
    }
}

FeedsViewModel生成[FeedViewModel]的fetchAllFeeds函数

private func fetchAllFeeds() {
    feedRepository.fetchAllFeeds() { [weak self] result in
        switch result {
        case .success(let allFeeds):
            if let user = self?.user {
                self?.feeds = allFeeds.compactMap { FeedViewModel($0, withUser: user) }
                self?.feeds.sort(by: { $0.updatedAt > $1.updatedAt })

                if let feeds = self?.feeds {
                    self?.followedFeeds = feeds.filter { user.following.contains($0.creator) }
                    
                    self?.ownFeeds = feeds.filter { $0.creator == user }
                    
                    self?.usedFeeds = feeds.filter { $0.activeUsers.contains(user) }
                }
            }
        case .failure(let error):
            print("Failed fetching feeds: \(error)")
        }
    }
}

FeedView生成FeedRow的代码

VStack { ForEach(self.feedsViewModel.feeds) { feed in FeedRow(feed) } } .padding(.horizontal)

原FeedRow代码

struct FeedRow: View {

    @EnvironmentObject var authenticationViewModel: AuthenticationViewModel
    @StateObject var feedViewModel: FeedViewModel

    @State private var showComments = false
    @State private var translationActive: Bool = false

    private var textIsTranslated: Bool {
        feedViewModel.translatedText != nil
    }

    init(_ feedViewModel: FeedViewModel) {
        _feedViewModel = StateObject(wrappedValue: feedViewModel)
    }

    var body: some View {
        VStack(spacing: 8) {
            HStack(alignment: .top, spacing: 16) {
                ProfilePictureSmall()
                
                VStack(spacing: 8) {
                    HStack {
                        Text(self.feedViewModel.creator.realName)
                            .font(.footnote)
                            .fontWeight(.semibold)
                        
                        Spacer()
                        
                        Button(action: {
                            
                        }, label: {
                            Image(systemName: "envelope")
                                .foregroundStyle(LinearGradient(gradient: Gradient(colors: [.blue, .red]), startPoint: .leading, endPoint: .trailing))
                        })
                    }
                    Text((!self.translationActive ? self.feedViewModel.text : self.feedViewModel.translatedText) ?? "")
                        .font(.footnote)
                        .fontWeight(.thin)
                        .frame(maxWidth: .infinity, alignment: .leading)
                    
                    HStack(alignment: .bottom, spacing: 16) {
                        VStack {
                            Button(action: {
                                self.feedViewModel.likeFeed()
                            }, label: {
                                Image(systemName: "heart")
                                    .foregroundStyle(LinearGradient(gradient: Gradient(colors: [.blue, .red]), startPoint: .leading, endPoint: .trailing))
                            })
                            Text(String(self.feedViewModel.likes.count))
                                .font(.footnote)
                                .fontWeight(.ultraLight)
                        }
                        
                        VStack {
                            Button(action: {
                                self.showComments = true
                            }, label: {
                                Image(systemName: "text.bubble")
                                    .foregroundStyle(LinearGradient(gradient: Gradient(colors: [.red, .blue]), startPoint: .leading, endPoint: .trailing))
                            })
                            Text(String(self.feedViewModel.comments.count))
                                .font(.footnote)
                                .fontWeight(.ultraLight)
                        }
                        
                        Spacer()
                        
                        VStack {
                            Button(action: {
                                self.translationActive.toggle()
                            }, label: {
                                Text(self.textIsTranslated ? (self.translationActive ? "Show original" : "Translate") : "No Translation")
                            })
                            .disabled(!self.textIsTranslated)
                            
                            Text(self.feedViewModel.createdAtString)
                                .fontWeight(.ultraLight)
                        }
                        .font(.footnote)
                    }
                }
            }
            
            if !self.feedViewModel.images.isEmpty {
                ScrollView(.horizontal, showsIndicators: false) {
                    LazyHStack(alignment: .center) {
                        ForEach(self.feedViewModel.images, id: \.self) { url in
                            AsyncImage(
                                url: URL(string: url),
                                content: { image in
                                    image
                                        .resizable()
                                        .scaledToFit()
                                        .frame(height: 200)
                                        .clipShape(RoundedRectangle(cornerRadius: 10))
                                },
                                placeholder: {
                                    Image(systemName: "network.slash")
                                }
                            )
                        }
                    }
                    .padding(.horizontal)
                }
                .frame(height: 200)
            }
            
            Divider()
            
            if !self.feedViewModel.richPreviews.isEmpty {
                ForEach(self.feedViewModel.richPreviews) { richLinkPreview in
                    RichLinkPreviewView(richPreviewViewModel: richLinkPreview)
                }
            }
        }
        .sheet(isPresented: $showComments, content: {
            CommentsView()
                .environmentObject(self.feedViewModel)
                .presentationDetents([.medium, .large])
        })
        .onAppear {
            self.feedViewModel.translateText()
        }
        .onChange(of: self.textIsTranslated) {
            if self.textIsTranslated {
                self.translationActive = true
            } else {
                self.translationActive = false
            }
        }
    }
}

无效尝试

在FeedsViewModel的闭包中使用[weak self],无法解决实例重复和数据不一致问题,因为问题根源不在内存泄漏的弱引用处理,而是ViewModel在视图中的持有方式错误。

问题根源

原代码中FeedRow使用@StateObject接收外部传入的FeedViewModel实例,但@StateObject的设计初衷是由视图自身创建并管理实例生命周期。当通过StateObject(wrappedValue:)传入外部实例时,SwiftUI会创建一个新的包装实例,导致原ViewModel和视图内部的ViewModel实例分离:

  1. 内存中出现两个独立的FeedViewModel实例;
  2. 视图显示的是内部包装实例的数据,和外部传入的原实例数据不同步,导致打印值不一致。

修复方案

将@StateObject替换为@ObservedObject——@ObservedObject用于观察外部传入的ViewModel实例,不会创建新实例,保证数据同步;同时添加弱引用避免潜在的循环引用:

@EnvironmentObject var authenticationViewModel: AuthenticationViewModel
weak var weakFeedViewModel: FeedViewModel?
@ObservedObject var feedViewModel: FeedViewModel

@State private var showComments = false
@State private var translationActive: Bool = false

private var textIsTranslated: Bool {
    feedViewModel.translatedText != nil
}

init(_ feedViewModel: FeedViewModel) {
    weakFeedViewModel = feedViewModel
    _feedViewModel = ObservedObject(wrappedValue: weakFeedViewModel ?? feedViewModel)
}

内容的提问来源于stack exchange,提问作者Philipp Henkel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 17:57:14