SwiftUI封装UICollectionViewCell内存泄漏与滚动卡顿问题求助
问题描述
开发SwiftUI应用时,因功能与性能需求采用UIKit的UICollectionView,单元格通过UIHostingController封装SwiftUI视图。初始滚动流畅,但滚动至末尾开始卡顿,每次重启表现一致;Xcode调试导航器显示内存随上下滚动持续增长,无法定位内存占用源。已制作Demo复现问题(卡顿程度弱于原应用,但持续滚动可见内存上升),并尝试复用UIHostingController优化,性能有所提升,但滚动一段时间后仍存在内存峰值与性能下降。
原代码实现
UICollectionViewWrapper(UIViewRepresentable)
struct UICollectionViewWrapper: UIViewRepresentable { var width: CGFloat var height: CGFloat @ObservedObject var viewModel: ViewModel func makeUIView(context: Context) -> UICollectionView { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = .clear collectionView.dataSource = context.coordinator collectionView.delegate = context.coordinator collectionView.alwaysBounceVertical = true collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "collectionViewCell") return collectionView } func updateUIView(_ uiView: UICollectionView, context: Context) { // } class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { var parent: UICollectionViewWrapper init(_ parent: UICollectionViewWrapper) { self.parent = parent } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return parent.viewModel.songs.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as! CollectionViewCell let song = parent.viewModel.songs[indexPath.row] cell.setUpCell(width: parent.width, height: parent.height, song: song) return cell } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: parent.width, height: parent.height) } } func makeCoordinator() -> Coordinator { Coordinator(self) } }
UICollectionViewCell
class CollectionViewCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setUpCell(width: CGFloat, height: CGFloat, song: Song ) { let host = UIHostingController(rootView: CoverView(song: song)) let uiView = host.view! uiView.frame = CGRect(x: 0, y: 0, width: width, height: height) addSubview(uiView) } }
ViewModel
class ViewModel: ObservableObject { @Published var songs = [Song]() func loadSongs() { for _ in 0..<100 { let song = Song(name: "Fix You", artist: "ColdPlay", album: "X&Y 2005", lyrics: """ When you try your best, but you don't succeed When you get what you want, but not what you need When you feel so tired, but you can't sleep Stuck in reverse And the tears come streaming down your face When you lose something you can't replace When you love someone, but it goes to waste Could it be worse? Lights will guide you home And ignite your bones And I will try to fix you """ ) songs.append(song) } } }
SwiftUI Views
struct CoverView: View { var song: Song var body: some View { ZStack { RoundedRectangle(cornerRadius: 15) .fill(.tint) VStack(alignment: .leading, spacing: 10) { Text("\(song.artist) - \(song.name)") .font(.system(size: 30, weight: .bold, design: .rounded)) Text(song.album) .font(.system(size: 22, weight: .medium, design: .rounded)) Spacer() Text(song.lyrics) .font(.system(size: 22, weight: .medium, design: .rounded)) } .padding() .padding(.top) .foregroundColor(.white) } } } struct ContentView: View { @StateObject var viewModel = ViewModel() var body: some View { GeometryReader { geometry in UICollectionViewWrapper(width: geometry.size.width, height: geometry.size.height, viewModel: viewModel ) .onAppear { viewModel.loadSongs() } } } }
已尝试的优化(UPDATE)
复用UIHostingController,但仍存在内存峰值与性能下降:
class CollectionViewCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private(set) var host: UIHostingController<CoverView>? func setUpCell(width: CGFloat, height: CGFloat, song: Song ) { if let host = host { host.rootView = CoverView(song: song) host.view.layoutIfNeeded() } else { let host = UIHostingController(rootView: CoverView(song: song), ignoreSafeArea: true) let uiView = host.view! uiView.frame = CGRect(x: 0, y: 0, width: width, height: height) addSubview(uiView) self.host = host } } }
解决方案
1. 修复单元格复用的内存泄漏问题
当前优化后的单元格存在两个核心问题:未在复用前清理旧视图引用、直接替换rootView导致SwiftUI视图层级残留。修改CollectionViewCell:
class CollectionViewCell: UICollectionViewCell { private var host: UIHostingController<CoverView>? override init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 单元格复用前强制清理资源 override func prepareForReuse() { super.prepareForReuse() host?.view.removeFromSuperview() host = nil } func setUpCell(width: CGFloat, height: CGFloat, song: Song) { let host = UIHostingController(rootView: CoverView(song: song), ignoreSafeArea: true) host.view.frame = bounds // 用bounds适配单元格尺寸,避免硬编码 addSubview(host.view) self.host = host host.view.layoutIfNeeded() // 提前布局,避免滚动延迟 } }
2. 优化SwiftUI视图的可更新性
让CoverView支持动态数据更新,减少视图树重建开销:
struct CoverView: View { @Binding var song: Song var body: some View { ZStack { RoundedRectangle(cornerRadius: 15) .fill(.tint) VStack(alignment: .leading, spacing: 10) { Text("\(song.artist) - \(song.name)") .font(.system(size: 30, weight: .bold, design: .rounded)) Text(song.album) .font(.system(size: 22, weight: .medium, design: .rounded)) Spacer() Text(song.lyrics) .font(.system(size: 22, weight: .medium, design: .rounded)) } .padding() .padding(.top) .foregroundColor(.white) } } // 绑定值初始化 init(song: Binding<Song>) { self._song = song } // 兼容原传值方式 init(song: Song) { self._song = .constant(song) } }
同时修改单元格数据传递方式,使用绑定值更新:
// Coordinator的cellForItemAt方法中 guard let parent = parent else { return cell } cell.setUpCell(width: parent.width, height: parent.height, song: $parent.viewModel.songs[indexPath.row]) // 对应修改CollectionViewCell的setUpCell方法参数 func setUpCell(width: CGFloat, height: CGFloat, song: Binding<Song>) { let host = UIHostingController(rootView: CoverView(song: song), ignoreSafeArea: true) host.view.frame = bounds addSubview(host.view) self.host = host host.view.layoutIfNeeded() }
3. 优化UICollectionView性能配置
在UICollectionViewWrapper中添加性能优化项:
func makeUIView(context: Context) -> UICollectionView { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical layout.estimatedItemSize = CGSize(width: width, height: height) // 开启预加载 let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = .clear collectionView.dataSource = context.coordinator collectionView.delegate = context.coordinator collectionView.alwaysBounceVertical = true collectionView.prefetchDataSource = context.coordinator // 开启数据预取 collectionView.showsVerticalScrollIndicator = false // 禁用不必要的指示器 collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "collectionViewCell") return collectionView }
让Coordinator实现预取协议:
class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDataSourcePrefetching { weak var parent: UICollectionViewWrapper? // 改为弱引用,避免循环引用 init(_ parent: UICollectionViewWrapper) { self.parent = parent } // 原有方法... func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { // 可在此预加载数据,当前Demo无需额外操作 } }
4. 定位内存泄漏工具
使用Xcode的Memory Graph Debugger捕获内存快照,查找UICollectionViewCell、UIHostingController的残留引用,重点排查循环引用(如Coordinator对parent的强引用)。
内容的提问来源于stack exchange,提问作者FE_Tech
相关产品推荐
相关产品推荐

