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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 16:35:17