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

PHCachingImageManager偶现无结果无报错挂起问题求助

解决PHCachingImageManager结合async/await请求图片时的挂起问题

我在使用PHCachingImageManager为图片网格视图加载缩略图时,采用了Swift async/await语法封装请求,但遇到了偶尔挂起的问题——请求既不返回结果也不报错,后续调用该函数的请求也会跟着挂起,且completion回调从未执行。相关代码如下:

class PhotoRequestManager {
  // Properties from class
  private var imageCachingManager = PHCachingImageManager()

  func fetchImage(
    asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode
  ) async throws -> UIImage? {
    
    let options = PHImageRequestOptions()
    options.deliveryMode = .opportunistic
    options.resizeMode = .fast
    options.isNetworkAccessAllowed = true
    options.isSynchronous = true

    return try await withCheckedThrowingContinuation { [weak self] continuation in
      self?.imageCachingManager.requestImage(
        for: asset, targetSize: targetSize, contentMode: contentMode, options: options
      ) { image, info in
        /// When the hang occurs, this block never gets executed.
        if let error = info?[PHImageErrorKey] as? Error {
          continuation.resume(throwing: error)
          return
        }
        continuation.resume(returning: image)
      }
    }
  }

}

// From UI
struct MySwiftUIView: View {
  @EnvironmentObject private var photosRequestManager: PhotoRequestManager

  var body: some View {
    ZStack {
      // Content
    }
    .task { await requestImage() }
  }

  @State private var thumbnail: Image?
  // 假设asset和size已定义
  let asset: PHAsset
  let size: CGSize

  private func requestImage() async {
    guard
      let uiImage = try? await photosRequestManager.fetchImage(
        asset: asset, targetSize: size, contentMode: .aspectFill)
    else {
      return
    }

    thumbnail = Image(uiImage: uiImage)
  }
}

核心问题分析

  • 同步请求与异步continuation冲突:设置options.isSynchronous = true后,requestImage会阻塞调用线程等待结果,但SwiftUI的.task默认在主线程执行,completion回调也需要主线程执行,最终形成死锁,导致continuation永远无法resume。
  • 未处理Task取消场景:View销毁时关联的.task会被取消,但原代码未监听取消事件,也未取消PHImage请求,导致continuation持续挂起。
  • weak self导致请求未触发:当self提前释放时,self?.imageCachingManager.requestImage不会执行,continuation没有被resume的机会。
  • 回调路径覆盖不全:未处理请求被取消、资产无效等场景,导致部分情况下回调无法触发。

修复方案

1. 移除同步请求配置

删除options.isSynchronous = true,让请求以异步方式执行,避免线程阻塞:

let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic
options.resizeMode = .fast
options.isNetworkAccessAllowed = true
// 移除 options.isSynchronous = true

2. 处理Task取消与请求取消

在continuation中监听Task取消事件,取消对应的PHImage请求,并确保所有路径都能resume continuation:

func fetchImage(
    asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode
) async throws -> UIImage? {
    let options = PHImageRequestOptions()
    options.deliveryMode = .opportunistic
    options.resizeMode = .fast
    options.isNetworkAccessAllowed = true

    return try await withCheckedThrowingContinuation { [weak self] continuation in
        guard let self = self else {
            continuation.resume(throwing: NSError(domain: "PhotoRequestManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "管理器已释放"]))
            return
        }
        
        // 保存请求ID用于后续取消
        let requestID = self.imageCachingManager.requestImage(
            for: asset, targetSize: targetSize, contentMode: contentMode, options: options
        ) { image, info in
            // 处理请求被取消的情况
            if let isCancelled = info?[PHImageCancelledKey] as? Bool, isCancelled {
                continuation.resume(throwing: CancellationError())
                return
            }
            
            // 处理请求错误
            if let error = info?[PHImageErrorKey] as? Error {
                continuation.resume(throwing: error)
                return
            }
            
            // 返回图片结果
            continuation.resume(returning: image)
        }
        
        // 监听Task取消,同步取消PHImage请求
        continuation.onCancel = {
            self.imageCachingManager.cancelImageRequest(requestID)
        }
    }
}

3. 增强SwiftUI侧错误处理

不要直接用try?忽略错误,添加错误日志方便排查:

private func requestImage() async {
    do {
        if let uiImage = try await photosRequestManager.fetchImage(
            asset: asset, targetSize: size, contentMode: .aspectFill
        ) {
            thumbnail = Image(uiImage: uiImage)
        }
    } catch {
        print("图片请求失败:\(error)")
    }
}

额外建议

  • 对于网格视图,使用PHCachingImageManager的startCachingImages(for:targetSize:contentMode:options:)提前缓存即将显示的资产,减少请求延迟。
  • 避免在主线程执行耗时的图片处理,必要时用Task.detached将转换逻辑放到后台,再切回主线程更新UI。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 13:23:39