SwiftUI照片库图片加载显示异常问题咨询与优化方案
照片网格加载异常问题
我已经实现了SwiftUI网格视图中加载并显示所有照片的功能,初始加载和滚动加载都正常,但持续滚动后图片会突然停止加载,只显示ProgressView;回滚到之前加载过的图片时,这些图片也消失了,同样显示ProgressView。怀疑是内存问题,但调试时内存占用并不高,难以排查。
PhotoThumbnailView 代码
struct PhotoThumbnailView: View { @State private var image: Image? @EnvironmentObject var photoLibraryService: PhotoLibraryService private var assetLocalId: String init(assetLocalId: String) { self.assetLocalId = assetLocalId } var body: some View { ZStack { // Show the image if it's available if let image = image { GeometryReader { proxy in image .resizable() .aspectRatio(contentMode: .fill) .frame( width: proxy.size.width, height: proxy.size.width ) .clipped() } .aspectRatio(1, contentMode: .fit) } else { Rectangle() .foregroundColor(.gray) .aspectRatio(1, contentMode: .fit) ProgressView() } } .task { await loadImageAsset() } .onDisappear { image = nil } } } extension PhotoThumbnailView { func loadImageAsset( targetSize: CGSize = PHImageManagerMaximumSize ) async { guard let uiImage = try? await photoLibraryService.fetchImage(byLocalIdentifier: assetLocalId,targetSize: targetSize) else { print("unable to get image") image = nil return } image = Image(uiImage: uiImage) } }
PhotoLibraryService 代码
class PhotoLibraryService: ObservableObject { @Published var results = PHFetchResultCollection(fetchResult: .init()) var imageCachingManager = PHCachingImageManager() var authorizationStatus: PHAuthorizationStatus = .notDetermined @Published var errorString : String = "" init() { PHPhotoLibrary.requestAuthorization { (status) in self.authorizationStatus = status switch status { case .authorized: self.errorString = "" self.fetchAllPhotos() case .denied, .restricted, .limited: self.errorString = "Photo access permission denied" case .notDetermined: self.errorString = "Photo access permission not determined" @unknown default: fatalError() } } } private func fetchAllPhotos() { imageCachingManager.allowsCachingHighQualityImages = false let fetchOptions = PHFetchOptions() fetchOptions.includeHiddenAssets = false fetchOptions.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: true) ] // Fetch all images and store in photos to perform extra code // on in the background thread DispatchQueue.global().async { let photos = PHAsset.fetchAssets(with: .image, options: fetchOptions) photos.enumerateObjects({asset, _, _ in // <extra code not relevant to this question> }) // Update main thread and UI DispatchQueue.main.async { self.results.fetchResult = photos } } } func fetchImage(byLocalIdentifier localId: String, targetSize: CGSize = PHImageManagerMaximumSize, contentMode: PHImageContentMode = .default) async throws -> UIImage? { let results = PHAsset.fetchAssets(withLocalIdentifiers: [localId], options: nil) guard let asset = results.firstObject else {throw PHPhotosError(_nsError: NSError(domain: "Photo Error", code: 2))} let options = PHImageRequestOptions() options.deliveryMode = .opportunistic options.resizeMode = .fast options.isNetworkAccessAllowed = true options.isSynchronous = true return try await withCheckedThrowingContinuation { [weak self] continuation in /// Use the imageCachingManager to fetch the image self?.imageCachingManager.requestImage( for: asset, targetSize: targetSize, contentMode: contentMode, options: options, resultHandler: { image, info in /// image is of type UIImage if let error = info?[PHImageErrorKey] as? Error { print(error) continuation.resume(throwing: error) return } continuation.resume(returning: image) } ) } } }
问题分析与修复方案
核心问题点
onDisappear清空图片导致重复加载:PhotoThumbnailView在消失时把image设为nil,每次视图滚回屏幕都要重新请求图片,完全浪费了PHCachingImageManager的缓存能力。- 请求原图引发加载拥堵:用
PHImageManagerMaximumSize请求缩略图会获取原始分辨率图片,不仅占用内存,还会让请求队列积压,导致后续图片加载阻塞。 PHCachingImageManager未配置缓存策略:没有利用它的预缓存/清理机制,无法提前加载即将显示的图片,也不能合理释放已离开屏幕的缓存。
具体修复步骤
1. 移除不必要的图片清空逻辑
删除 PhotoThumbnailView 中的 .onDisappear { image = nil },让视图保留已加载的图片引用,依赖系统和 PHCachingImageManager 的内存管理自动释放。
2. 调整缩略图请求尺寸
根据网格单元格的实际大小请求适配的缩略图(考虑Retina屏,用2倍尺寸),避免加载原图:
// 修改PhotoThumbnailView的loadImageAsset方法 func loadImageAsset(targetSize: CGSize = CGSize(width: 200, height: 200)) async { guard let uiImage = try? await photoLibraryService.fetchImage(byLocalIdentifier: assetLocalId, targetSize: targetSize) else { print("unable to get image") image = nil return } image = Image(uiImage: uiImage) }
3. 优化图片请求选项
在 PhotoLibraryService 的 fetchImage 方法中,针对缩略图调整请求参数,优先加载低分辨率图,避免异步阻塞:
let options = PHImageRequestOptions() options.deliveryMode = .fastFormat // 替换.opportunistic,优先加载快速格式 options.resizeMode = .fast options.isNetworkAccessAllowed = true options.isSynchronous = false // 改为异步请求,避免阻塞任务队列
4. 添加请求状态防重复
在 PhotoThumbnailView 中增加加载状态标记,防止同一视图重复发起请求:
@State private var isLoading = false // 修改task部分 .task { guard !isLoading else { return } isLoading = true await loadImageAsset() isLoading = false }
5. 配置 PHCachingImageManager 缓存范围
在 PhotoLibraryService 中添加缓存管理方法,监听网格可见区域,提前缓存前后范围的图片,同时清理超出范围的缓存:
private var cachedAssets: [PHAsset] = [] func updateCachedAssets(for visibleAssets: [PHAsset], targetSize: CGSize) { let options = PHImageRequestOptions() options.deliveryMode = .fastFormat options.resizeMode = .fast // 开始缓存可见区域及前后的图片 imageCachingManager.startCachingImages(for: visibleAssets, targetSize: targetSize, contentMode: .aspectFill, options: options) // 清理不在可见范围内的缓存 let assetsToStopCaching = cachedAssets.filter { !visibleAssets.contains($0) } imageCachingManager.stopCachingImages(for: assetsToStopCaching, targetSize: targetSize, contentMode: .aspectFill, options: options) cachedAssets = visibleAssets }
内容的提问来源于stack exchange,提问作者Nat
相关产品推荐
相关产品推荐

