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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 23:25:25