본문 바로가기
이모저모/AVFoundation

iOS 앨범 이미지 Fetch - PHCachingImageManager로 성능 향상

by ARpple 2025. 3. 24.

문제 인식

 

What is Task Continuation Misuse??

  • Continuation을 적절히 호출하지 않거나 여러 번 호출하는 경우에 Swift 런타임이 감지하여 발생하는 오류
  • 기존 completionHandler나 delegate에서 처리를 받은 작업의 흐름에 문제가 생겼다는 것임
    ⇒ 문제 발생의 경우
    • resume을 반드시 한 번만 호출했는가?
    • 모든 코드 흐름에서 resume 이 호출되도록 보장했는가?
    • resume을 호출하기 전에 비동기 작업이 완료되었는가?
Task Continuation Misuse에 대한 자세한 설명은 다음 링크에...

https://velog.io/@dvhuni/Continuation

 

Continuation

안녕하세요~!!! 🥰 이번 포스트는 Concurrency !! 그중에서도 Continuation에 대해 알아보겠습니다!! 🤓

velog.io

기존 코드

Thumbnail 추출하는 메서드: .convertToUIImage라는 메서드를 통해서 크기를 조절한 이미지를 반환한다.

 

actor ThumbnailExecutor{
    private var result: PHFetchResult<PHAsset>!
    ...
    func run() async{
        ... // PHFetchResult<PHAsset> 타입을 통해 사용자가 고른 이미지들을 찾는다.
        result.enumerateObjects(options:.concurrent) { asset, _, _ in
      Task{
                  // 각각의 Asset을 UIImage로 변환한다.
            let image = try await asset.convertToUIImage(size: .init(width: 120, height: 120 * 1.3333)) 
                        ...                        
        }
      }
    }

결국 핵심 문제는 .convertToUIImage 메서드인데… 사실 이건 내가 직접 만든 메서드이다

convertToUIImage(size:CGSize? = nil) async throws -> UIImage

extension PHAsset{
    func convertToUIImage(size:CGSize? = nil) async throws -> UIImage{
        return try await withCheckedThrowingContinuation { [weak self] continuation in
            self?.**requestContentEditingInput**(with: nil) { input, info in
                guard let input, let imageURL = input.fullSizeImageURL else {return}
                let imageSourceOption = [kCGImageSourceShouldCache: false] as CFDictionary
                let imageSource: CGImageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOption)!
                let image = self!.coreDownSample(resource: imageSource,size: size)
                continuation.resume(returning: image)
            }
        }
    }
  }

✅ 코드 설명

  1. withCheckedThrowingContinuation() ⇒ 전통적인 completionHandler를 이용한 비동기처리를 async-await 구문으로 바꾸어주는 swift 기본 메서드
  2. requestContentEditingInput()PHAsset를 통해 PHContentEditingInput 과 추가 정보 데이터로 반환하는 메서드
  3. PHContentEditingInput 값을 CGImageResource로 바꾼다.
  4. CGImageResource로 담긴 이미지를 스크린 사이즈에 맞게 DownSampling 후 UIImage로 반환한다.

➕ 추가적인 코드 설명

  1. coreDownSample은 CGImageSource로 부터 CGImage → UIImage로 변환하는 메서드다.

해결 방법

💡 기존 코드에 PHContentEditingInputRequestOptions()의 isNetworkAccessAllowed = true 추가

isNetworkAccessAllowed??

➡️ iOS에서 iCloud로 저장한 이미지를 가져오는 것을 허용한다.

 

func convertToUIImage(size:CGSize? = nil) async throws -> UIImage{
        return try await withCheckedThrowingContinuation { [weak self] continuation in
            let options = PHContentEditingInputRequestOptions()
            options.isNetworkAccessAllowed = true
            self?.requestContentEditingInput(with: options) { input, info in
                guard let input, let imageURL = input.fullSizeImageURL else {return}
                let imageSourceOption = [kCGImageSourceShouldCache: false] as CFDictionary
                let imageSource: CGImageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOption)!
                let image = self!.coreDownSample(resource: imageSource,size: size)
                continuation.resume(returning: image)
            }
        }
    }

너무 느린 문제가 발생했다,,, 느리다면 Task Continuation Misuse 문제의 원인이 될 수 있다는 문제도 있다..!

🥲 저 이미지는 2배속 한 이미지...

⇒ resume을 가져오지 못해..!

개선 방법 #1 convertToUIImage 처리를 담당하는 고유 객체 생성하기

final class PHAssetToUIImageContainer {

    private var checkedContinuation : CheckedContinuation<UIImage, any Error>?

    private func _convertToUIImage(phAsset: PHAsset,size: CGSize? = nil){
        phAsset.requestContentEditingInput(with: nil) { input, info in
            guard let input, let imageURL = input.fullSizeImageURL else {return}
            let imageSourceOption = [kCGImageSourceShouldCache: false] as CFDictionary
            let imageSource: CGImageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOption)!
            let image = phAsset.coreDownSample(resource: imageSource,size: size)
            self.checkedContinuation?.resume(returning: image)
            self.checkedContinuation = nil
        }
    }

    func convertToUIImage(size:CGSize? = nil) async throws -> UIImage {
        return try await withCheckedThrowingContinuation { continuation in
            self.checkedContinuation = continuation
        }
    }
}
  • 일반적인 Task Continuation Misuse와 관련된 구글링을 한다면 얻는 간단한 해결법
  • continuation 을 특정 객체가 옵셔널 값으로 가져 잘못된 경우에 대한 처리를 nil 로 반환함, 비동기에 대한 근본적인 문제 해결이라 보긴 어려움
  • PHAsset의 Extension 메서드로 동작하지 못하고 굳이 새로운 객체를 생성하는 방법을 사용해야하는지 의문
  • 근본적으로 requestContentEditingInput 에 대한 처리가 잘못되어서 저런 오류를 뱉는것이 아닐까? Swift 컴파일러가 바보가 아닌이상!!

⇒ 다른 방법을 찾아보기로 함

✅ 개선 방법 #2 PHCachingImageManager 사용하기

 

PHCachingImageManager | Apple Developer Documentation

An object that facilitates retrieving or generating preview thumbnails, optimized for batch preloading large numbers of assets.

developer.apple.com

 

"미리보기 썸네일을 쉽게 검색하거나 생성할 수 있는 오브젝트로, 많은 수의 에셋을 일괄적으로 미리 로드하는 데 최적화되어 있습니다."

➕ 개별 에셋에 대한 이미지가 필요한 경우 요청
 "이미 (for:targetSize:contentMode:options:resultHandler:) 메서드를 호출하고 해당 에셋을 준비할 때 사용한 것과 동일한 파라미터를 전달합니다."

✅ 공식 문서 overview 4번

Thumbnail을 추출하는 메서드

actor ThumbnailExecutor{
    private var result: PHFetchResult<PHAsset>!
    private let imageManager: PHCachingImageManager = .init()
    ...
    func run() async{
        ... // PHFetchResult<PHAsset> 타입을 통해 사용자가 고른 이미지들을 찾는다.
        result.enumerateObjects(options:.concurrent) { asset, _, _ in
            self.fetchImage(phAsset: asset, size: .init(width: 3 * 120, height: 3 * 120 * 1.77), contentMode: .aspectFill) { 
                        ...
        }
      }
    }

    private func fetchImage(phAsset: PHAsset,
                            size: CGSize,
                            contentMode: PHImageContentMode,
                            completion: @escaping (UIImage) -> Void) {
        let options = PHImageRequestOptions()

        options.isNetworkAccessAllowed = true // iCloud
        options.deliveryMode = .highQualityFormat

        let reqestHandler :(UIImage?, [AnyHashable : Any]?) -> Void = { image, _ in
            guard let image else { return }
            completion(image)
        }
        imageManager.requestImage(for: phAsset,
                                  targetSize: size,
                                  contentMode: contentMode,
                                  options: options,
                                  resultHandler:reqestHandler
        )
    }
  • fetchImage는 PHCachingImageManagerrequestImage 메서드를 호출하는데 필요한 여러 구성 로직을 감싼다.

개선방법 #2를 적용한 최종 결과

 

  • PHCachingImageManager 를 사용한 결과, 최소 95%의 응답속도의 성능 향상을 보여줘 빠르게 이미지들을 가져오는 것을 확인할 수 있다.
  • resultHandler 에서 UIImage 반환 타입이 nil 이므로 반환하지 못할 가능성이 있다.
  • 뭐 iCloud에 이미지를 가져올 때, 비행기 모드를 켠다거나 그런 오류일 수도??
  • 나중에 사용자에게 이런 이미지를 가져오지 못한 에러 처리에 대한 안내를 할 필요가 있겠다.

댓글