SwiftUI从AWS 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时,内存会更快被回收。
最后验证
做完以上修改后,再次测试:
- 滚动LazyVGrid,观察控制台是否打印
ImageLoader deinit日志 - 用Xcode的Memory Graph Debugger查看内存占用,确认没有内存泄漏
- 滚动后内存应该会在视图消失后逐渐下降,不会持续飙升
内容的提问来源于stack exchange,提问作者mufumade

