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

SwiftUI LazyVGrid内存分配问题及UIKit实现排障

问题:SwiftUI LazyVGrid内存未释放与UIKit CollectionView空白屏问题

问题背景

开发SwiftUI应用时,在GalleryView中使用LazyVGrid以每行3个的布局展示应用缓存中的媒体文件,遇到以下问题:

  • LazyVGrid会懒加载媒体到内存,但移出视野的文件不会自动释放,当媒体数量>10时内存占用明显飙升
  • 尝试用自定义UIKit UICollectionView替代LazyVGrid,却出现空白屏,无法正常展示媒体项
  • 试过List替代、分块布局等方案,均未从根本解决内存问题

原有SwiftUI代码

struct GalleryView: View {
    @Binding var mediaFiles: [Media]
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                let width = geometry.size.width / 3 - 10
                let gridItemLayout = [GridItem(.fixed(width)), GridItem(.fixed(width)), GridItem(.fixed(width))]
                LazyVGrid(columns: gridItemLayout, spacing: 10) {
                    ForEach(mediaFiles, id: \.id) { media in
                        if let image = media.image {
                            Image(uiImage: image)
                                .resizable()
                                .scaledToFit()
                                .frame(width: width)
                        } else if let videoURL = media.videoURL {
                            // Display video thumbnail here
                        }
                    }
                }
            }
        }
    }
}

struct Media: Identifiable {
    let id = UUID()
    let creationTime: Date
    var image: UIImage?
    var videoURL: URL?
}

尝试的自定义UIKit代码

struct CollectionView: UIViewControllerRepresentable {
    typealias UIViewControllerType = UICollectionViewController

    @Binding var mediaFiles: [Media]
    var width: CGFloat

    func makeUIViewController(context: Context) -> UICollectionViewController {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: width / 3, height: width / 3)
        layout.minimumLineSpacing = 10
        layout.minimumInteritemSpacing = 10
        let collectionViewController = UICollectionViewController(collectionViewLayout: layout)
        collectionViewController.collectionView?.register(MediaCell.self, forCellWithReuseIdentifier: "cell")
        collectionViewController.collectionView?.backgroundColor = .white
        return collectionViewController
    }

    func updateUIViewController(_ uiViewController: UICollectionViewController, context: Context) {
        uiViewController.collectionView?.dataSource = context.coordinator
        uiViewController.collectionView?.delegate = context.coordinator
        uiViewController.collectionView?.reloadData()
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
        var parent: CollectionView

        init(_ parent: CollectionView) {
            self.parent = parent
        }

        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return parent.mediaFiles.count
        }

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MediaCell
            let media = parent.mediaFiles[indexPath.row]
            if let image = media.image {
                cell.imageView.image = image
            }
            return cell
        }


        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            // Handle selection here
        }
    }
}

解决方案

一、修复LazyVGrid内存释放问题的简单方案

不需要切换到UIKit,通过延迟加载+主动释放资源的方式从根本优化内存:

核心思路

不在Media结构体中缓存UIImage实例,只存储图片的本地缓存路径/标识;在视图需要展示时才加载图片,利用onDisappear生命周期钩子主动释放不再显示的图片资源,让内存复杂度降到O(k)(k为当前屏幕可见的媒体数量)。

修改后的代码

1. 更新Media结构体

struct Media: Identifiable {
    let id = UUID()
    let creationTime: Date
    let imageCachePath: String? // 存储本地缓存路径,而非UIImage实例
    let videoURL: URL?
}

2. 自定义可自动释放的ImageView

struct AutoReleaseImageView: View {
    let cachePath: String
    @State private var loadedImage: UIImage?
    
    var body: some View {
        Group {
            if let image = loadedImage {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
            } else {
                ProgressView()
                    .onAppear {
                        // 从缓存路径加载图片
                        guard let fileURL = URL(fileURLWithPath: cachePath),
                              let imageData = try? Data(contentsOf: fileURL),
                              let image = UIImage(data: imageData) else {
                            return
                        }
                        loadedImage = image
                    }
                    .onDisappear {
                        // 视图消失时主动释放图片内存
                        loadedImage = nil
                    }
            }
        }
    }
}

3. 优化后的GalleryView

struct GalleryView: View {
    @Binding var mediaFiles: [Media]
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                // 调整间距计算,避免布局溢出
                let itemWidth = (geometry.size.width - 20) / 3
                let gridLayout = Array(repeating: GridItem(.fixed(itemWidth)), count: 3)
                
                LazyVGrid(columns: gridLayout, spacing: 10) {
                    ForEach(mediaFiles, id: \.id) { media in
                        if let path = media.imageCachePath {
                            AutoReleaseImageView(cachePath: path)
                                .frame(width: itemWidth)
                        } else if let videoURL = media.videoURL {
                            // 视频缩略图复用相同逻辑:只存路径,按需加载+onDisappear释放
                            VideoThumbnailView(videoURL: videoURL)
                        }
                    }
                }
                .padding(.horizontal, 5)
            }
        }
    }
}

二、修复UIKit CollectionView空白屏问题

你的自定义CollectionView代码存在3个关键问题,修复后即可正常显示:

修复要点

  1. 布局边距缺失:默认CollectionView没有设置sectionInset,导致内容可能被边缘遮挡
  2. 代理设置时机错误:在updateUIViewController中重复设置数据源/代理会导致布局异常
  3. MediaCell未正确实现:缺少Cell内部UIImageView的布局逻辑

修复后的完整代码

1. 实现MediaCell

class MediaCell: UICollectionViewCell {
    let imageView = UIImageView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupImageView()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupImageView()
    }
    
    private func setupImageView() {
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFit
        contentView.addSubview(imageView)
        
        NSLayoutConstraint.activate([
            imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
            imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        // 复用Cell时清空图片,避免内存泄漏和旧内容残留
        imageView.image = nil
    }
}

2. 修复CollectionView UIViewControllerRepresentable

struct CollectionView: UIViewControllerRepresentable {
    typealias UIViewControllerType = UICollectionViewController

    @Binding var mediaFiles: [Media]
    let width: CGFloat

    func makeUIViewController(context: Context) -> UICollectionViewController {
        let layout = UICollectionViewFlowLayout()
        let itemWidth = (width - 20) / 3 // 减去总间距(10*2)
        layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
        layout.minimumLineSpacing = 10
        layout.minimumInteritemSpacing = 10
        layout.sectionInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) // 添加左右边距
        
        let collectionVC = UICollectionViewController(collectionViewLayout: layout)
        collectionVC.collectionView?.register(MediaCell.self, forCellWithReuseIdentifier: "cell")
        collectionVC.collectionView?.backgroundColor = .systemBackground
        // 初始化时一次性设置数据源和代理
        collectionVC.collectionView?.dataSource = context.coordinator
        collectionVC.collectionView?.delegate = context.coordinator
        return collectionVC
    }

    func updateUIViewController(_ uiViewController: UICollectionViewController, context: Context) {
        // 仅在数据变化时刷新,避免重复设置代理
        uiViewController.collectionView?.reloadData()
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
        var parent: CollectionView

        init(_ parent: CollectionView) {
            self.parent = parent
        }

        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return parent.mediaFiles.count
        }

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MediaCell
            let media = parent.mediaFiles[indexPath.row]
            if let image = media.image {
                cell.imageView.image = image
            }
            return cell
        }

        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            // 处理选中逻辑
        }
    }
}

3. GalleryView中使用修复后的CollectionView

struct GalleryView: View {
    @Binding var mediaFiles: [Media]
    @StateObject private var model = MediaModel() // 假设你的数据模型是ObservableObject
    
    var body: some View {
        GeometryReader { geometry in
            CollectionView(mediaFiles: $mediaFiles, width: geometry.size.width)
                .frame(width: geometry.size.width, height: geometry.size.height)
                .onAppear {
                    model.loadMediaFiles()
                }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 13:44:53