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

SwiftUI从AWS S3下载并展示图片时内存占用过高问题求助

解决SwiftUI LazyVGrid展示S3图片的内存泄漏问题

看起来你遇到的核心问题是:图片视图消失后内存没有被正确释放,导致滚动后内存持续飙升最终崩溃。结合你的代码和现象(磁盘缓存时正常),我来一步步分析可能的原因和解决方案:

1. 先确认LazyVGrid是否正确回收了Cell

LazyVGrid只会销毁完全移出可视区域的Cell,这依赖于ForEach的唯一标识。如果你的MediaItem没有正确实现Identifiable,或者id重复,LazyVGrid会复用Cell而不是销毁,导致ImageView和它的ImageLoader一直留在内存里。

修复方案:
确保ForEach使用唯一的id(比如你的mid字段):

ForEach(mediaManager.mediaItems, id: \.mid) { item in
    VStack{
        ImageView(downloader: ImageLoader(mediaItem: item, size: .large, parentAlbum: nil))
    }
}

同时在ImageLoader的deinit里加个日志,验证它是否被销毁:

deinit {
    print("ImageLoader deinit for mid: \(mediaItem.mid)")
    cancel()
}

如果控制台没有打印这条日志,说明ImageLoader没有被销毁,问题出在生命周期或引用循环上。

2. 修复ImageLoader的Cancel逻辑,销毁Combine订阅

你当前的cancel()方法只取消了订阅,但没有将cancellable置为nil——AnyCancellable会持有订阅的引用,间接导致ImageLoader和UIImage无法被ARC回收。

修复方案:
修改cancel()方法:

func cancel() {
    cancellable?.cancel()
    cancellable = nil // 关键:释放订阅的引用,打破循环
    image = nil // 显式置空UIImage,帮助内存回收
}

3. 主动取消S3下载任务(最关键的点!)

你的Combine代码只取消了订阅,但AWSS3TransferUtility的下载任务并没有被取消——即使订阅取消,S3的下载任务可能还在后台继续下载数据,并且持有数据的引用,导致内存无法释放。这也是为什么磁盘缓存时内存正常(磁盘缓存不会有后台下载任务)。

第一步:修改S3CacheFetcher的download方法,返回下载任务

private func download(mediaItem: KeyType) -> AnyPublisher<(OutputType, AWSS3TransferUtilityDownloadTask?), Error>{
    let BUCKET = "someBucket"
    return Deferred {
        Future { promise in
            guard let key = S3CacheFetcher.getItemKey(mediaItem: mediaItem) else {
                fatalError("UserPoolID Error")
            }
            print("Downloading image with key: \(key)")
            // 创建并保存下载任务
            let task = AWSS3TransferUtility.default().downloadData(
                fromBucket: BUCKET,
                key: key,
                expression: nil
            ) { (task, url, data, error) in
                if let error = error{
                    print(error)
                    promise(.failure(error))
                }else if let data = data{
                    let encrypt = S3CacheFetcher.encrypt(data: data)
                    let decrypt = S3CacheFetcher.decrypt(data: encrypt)
                    promise(.success((decrypt as NSData, task)))
                }
            }
        }
    }
    .eraseToAnyPublisher()
}

记得同步修改get方法的返回类型:

func get(_ key: KeyType) -> AnyPublisher<(OutputType, AWSS3TransferUtilityDownloadTask?), Error> {
    return download(mediaItem: key).eraseToAnyPublisher()
}

第二步:在ImageLoader中持有并取消S3任务

class ImageLoader: ObservableObject {
    // 新增:持有S3下载任务引用
    private var s3DownloadTask: AWSS3TransferUtilityDownloadTask?
    // ... 其他属性不变

    func load() {
        guard !isLoading else { return }
        cancellable = S3CacheFetcher().get(.init(
            parentAlbum: self.parentAlbum,
            size: self.size,
            cipher: self.mediaItem.cipher,
            ivNonce: self.mediaItem.ivNonce,
            mid: self.mediaItem.mid
        ))
        .map{ data, task -> (UIImage?, AWSS3TransferUtilityDownloadTask?) in
            // 转成UIImage,同时返回任务
            return (UIImage(data: data as Data), task)
        }
        .replaceError(with: (nil, nil))
        .handleEvents(
            receiveOutput: { [weak self] image, task in
                self?.s3DownloadTask = task // 保存下载任务
            },
            receiveCompletion: { [weak self] _ in
                self?.onFinish()
                self?.s3DownloadTask = nil // 完成后释放任务
            },
            receiveCancel: { [weak self] _ in
                self?.onFinish()
                self?.s3DownloadTask?.cancel() // 取消S3下载任务
                self?.s3DownloadTask = nil
            }
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] image, _ in
            self?.image = image
        }
    }

    func cancel() {
        cancellable?.cancel()
        cancellable = nil
        s3DownloadTask?.cancel() // 主动取消S3下载任务
        s3DownloadTask = nil
        image = nil
    }
}

4. 优化UIImage的内存占用

UIImage初始化时会自动缓存解码后的像素数据,这会大幅增加内存占用。你可以手动解码图像,避免系统的额外缓存:

修改load方法中的map部分:

.map{ data, task -> (UIImage?, AWSS3TransferUtilityDownloadTask?) in
    guard let originalImage = UIImage(data: data as Data, scale: UIScreen.main.scale) else {
        return (nil, task)
    }
    // 手动解码图像
    UIGraphicsBeginImageContextWithOptions(originalImage.size, false, originalImage.scale)
    originalImage.draw(at: .zero)
    let decodedImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return (decodedImage, task)
}

手动解码后,当image被置为nil时,内存会更快被回收。

最后验证

做完以上修改后,再次测试:

  1. 滚动LazyVGrid,观察控制台是否打印ImageLoader deinit日志
  2. 用Xcode的Memory Graph Debugger查看内存占用,确认没有内存泄漏
  3. 滚动后内存应该会在视图消失后逐渐下降,不会持续飙升

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 15:52:43