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实例分离:
- 内存中出现两个独立的FeedViewModel实例;
- 视图显示的是内部包装实例的数据,和外部传入的原实例数据不同步,导致打印值不一致。
修复方案
将@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
相关产品推荐
相关产品推荐

