SimpleImageProvider

1.0.4

iOS개발시 사용할 수 있는 간편한 이미지 캐싱 라이브러리입니다.
J0onYEong/SimpleImageProvider

What's New

[1.0.4] Deadlock문제 해결, SwiftUI, UIKit활용 코드 수정

2025-02-25T12:42:41Z

변경된 점

데드락 문제해결

이전 버전의 코드는 Swift concurrencyDispatchQueue를 혼용하여 사용하고 있었습니다.

DispatchQueue의 경우 성능을 높이기 위해 기본적으로 동시성 큐를 사용하고 읽기작업의 경우 sync로 작업을

쓰기 작업에 한해서만 barrier플래그를 사용하여 값타입에 대한 동시성 문제를 방지하였습니다.

하지만, 몇몇 경우에 데드락이 발생하는 문제가 있었습니다.

원인을 분석한 결과 DispatchQueue에 작업이 무한대기 하는 현상이 지속적으로 관찰되었습니다.

디버깅 걀과 Task가 DispatchQueue에 sync작업을 호출하는 경우에 발생하는 것을 확인할 수 있었습니다.

이 현상은 DispatchQueue사용시 주의해야 하는 부분과 비슷한 문제로, 동기작업을 등록하는 쓰레드와 동기작업이 실행되는 쓰레드가 같은 경우 발생하는 데드락 현상이라고 생각됩니다.

DispatchQueue의 경우 위와 같은 문제를 피하기 위해 서로다른 쓰레드 풀을(qos변경을 통한) 사용하는 등 할 수 있지만, Swift concurrency와 GCD의 쓰레드 관리 차이로 인해 발생한 문제로 생각됩니다.

결론

해당 문제를 해결하기 위해 DispatchQueue를 삭제하고 모든 동시성 처리 과정을 Actor로 변경했습니다.

Simpe image provider

간편하게 이미지를 UI로 표현할 수 있는 이미지 캐싱 라이브러리입니다.

항목 내용
지원 iOS 버전 iOS 15.0 이상
지원 Swift 버전 5.9 이상
사용한 기술 Swift, Swift Concurrency

SimpeImageProvider 라이브러리를 사용중인 서비스

항목 링크
케어밋 앱스토어
soma코인뷰어 깃허브

how to use

SimpleImageProvider.shared 싱글톤 객체에 접근하여 해당 라이브러리의 기능을 사용할 수 있습니다.

메모리 캐싱 사이즈, 디스크 캐싱 사이즈, 오버플로우시 정책을 앱을 시작하기전 설정하길 권장합니다.

해당 코드를 다른 런타임 공간에서 실행할 경우 예기치 못한 동시성 문제가 발발할 수 있습니다.

class AppDelegate: NSObject, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

        SimpleImageProvider.shared.requestConfigureState(
            maxCacheImageCount: 50,
            maxDiskImageCount: 100,
            percentToDeleteWhenDiskOverflow: 20.0
        )
        
        return true
    }
}

SwiftUI 코드

SwiftUI에서 사용가능한 SimpleImage View가 구현되어 있습니다.

LazyVGrid(columns: columns) {
    
    ForEach(items, id: \.self) { item in
        
        SimpleImage(
            url:"your image url",
            size: .init(width: 100, height: 100),
            fadeOutduration: 0
        )
            .frame(width: 100, height: 100)
            .background(Color.gray.opacity(0.5))
            .cornerRadius(5)
    }
}

UIkit 코드

UIImageView의 확장자로 간편하게 접근이 가능합니다.

func setImage(url: String, size: CGSize) {
    
    uiImageView
        .simple
        .setImage(url: url, size: size, fadeOutDuration: 0.2)
}

코드로 접근

UI라이브러리가 아닌 코드로 접근하는 경우 싱글톤 객체를 활용하시길 바랍니다.

싱글톤 객체 자체는 동시성에 최적화되어 있습니다.

await SimpleImageProvider.shared.requestImage(...)

Tech features

  • 메모리 캐싱 NSCache를 사용하여 자주사용되는 이미지를 메모리에 캐싱합니다.

  • 디스크 캐싱 FileManager를 사용하여 이미지를 디스크에 캐싱하고 LRU방식으로 관리합니다.

  • 다운 샘플링 이미지 요청시 전달한 CGSize를 사용하여 전달 받은 이미지를 샘플링 합니다.

    ※ 샘플링이 완료된 이미지가 캐싱됩니다.

사이즈 기반 이미지 캐싱

디스크 및 메모리에 캐싱되는 이미지는 다운 샘플링이 적용된 이미지가 캐싱됩니다.

동일한 이미지이지만 샘플링 결과에따라 다르게 캐싱을 진행합니다.

아래 캐싱키 값에 다운샘플링 사이즈가 포함되는 것을 확인할 수 있습니다.

func createKey(url: String, size: CGSize?) -> String {
    
    var keyString = url
    
    if let size {
        
        let width = size.width
        let height = size.height
        
        keyString += "\(width)x\(height)"
    }
    
    return keyString
}

다운 샘플링

이미지 다운 샘플링은 다운로드 받은 데이터 버퍼를 CGImageSource로 변경합니다.

CGImageSource를 특정 사이즈의 썸네일(원본보다 작은/축약된 이미지)로 변경하는 작업을 통해 다운 샘플링을 진행합니다.

※ 이미지 캐싱을 직접관리하기 위해 이미지소스생성시와 썸네일 생성시 추가적인 캐싱옵션을 모두 해제했습니다.

func downSamplingImage(dataBuffer: Data, size: CGSize) async -> UIImage? {
    
    let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
    
    guard let imageSource = CGImageSourceCreateWithData(dataBuffer as CFData,
imageSourceOptions) else {
        
        return nil
    }
    
    let biggerLength = max(size.width, size.height)
    let scale = await UIScreen.main.scale
    let maxDimensionInPixels = biggerLength * scale
    let downsampleOptions = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceShouldCacheImmediately: false,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
    ] as CFDictionary
    
    guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0,
downsampleOptions) else {
        return nil
    }
    let image = UIImage(cgImage: downsampledImage)
    
    return image
}

플로우 차트

---
title: SimpleImageProvider flowchart
---
flowchart LR

    requestimage --> SimpleImageProvider

    SimpleImageProvider --> checkMemoryCache

    checkMemoryCache --> checkDiskCache

    checkDiskCache --> ImageDownloader

    ImageDownloader --> CacheImage
Loading

이미지 획득과정은 다음과 같이 진행되며 이미지 획득 실패시 다음단계로 넘어갑니다.

  1. 이미지 요청
  2. 메모리 캐시 체크
  3. 디스크 캐시 체크(디스크 캐시 발견시 해당 이미지를 곧바로 메모리에 캐싱합니다.)
  4. 이미지 다운로드
  5. 다운로드된 이미지 캐싱

오브젝트 맵

---
title: SimpleImageProvider class diagram
---
classDiagram
    class SimpleImageProvider {
        + requestImage(url, size) UIImage?
        + requestConfigureState(cacheSize)
    }

    class ImageModifier {
        + downSamplingImage(data, size) UIImage?
        + convertDataToUIImage(data)
    }

    class ImageDownloader {
        + requestImageData(url) Data?
    }

    class ImageCacher {

        + requestImage(url, size) UIImage?
        + cacheImage(url, size, image)
    }

    class DiskCacher
    class MemoryCacher

    ImageCacher o-- DiskCacher
    ImageCacher o-- MemoryCacher

    SimpleImageProvider --|> MemoryCacher : 1.check memeory cache
    SimpleImageProvider --|> DiskCacher : 2.check disk cache
    SimpleImageProvider --|> ImageDownloader : 3.request download & data buffer
    SimpleImageProvider --|> ImageModifier : 4.request image downsampling or convert data to image
Loading

Description

  • Swift Tools 5.9.0
View More Packages from this Author

Dependencies

  • None
Last updated: Thu May 15 2025 02:47:46 GMT-0900 (Hawaii-Aleutian Daylight Time)