본문 바로가기

iOS/PROJECT

[burstcamp] 이미지 캐시 개선하기 (NSCache Limit, DownSampling)

이미지 클릭시 burstcamp githunb로 이동합니다.

 

 

처음 Image Cache를 구현했을 때 사용한 방식은 다음과 같습니다.

 

프로필 사진의 경우

1. memory cache (NSCache)

2. disk cache (File Manager)

3. Network 요청 후 memory, disk cache 저장

 

피드 이미지의 경우

1. memory cache (NSCache)

2. Network 요청 후 memory, disk cache 저장

최대 250MB

 

개선점

1. NSCache Limit

NSCache는 메모리를 많이 사용할 때 자동삭제 정책을 지원하지만, limit을 지정해주지 않으면 다 쓸 때까지 써버리는? 문제가 있었습니다.

위의 사진처럼 사용하면 할수록 올라가는 메모리..

var countLimit: Int
var totalCostLimit: Int

 

최대 몇개, 용량을 지정해주면 지정해준 값이 넘어갈 때 자동으로 삭제가 된다는 것을 알 수 있습니다.

 

테스트로 10개, 10MB로 설정해주고 메모리 사용량을 보았습니다.

final class ImageCacheManager: NSObject, NSCacheDelegate {

    static let shared = ImageCacheManager(countLimit: 10, totalCostLimit: 1024 * 1024 * 10)
    // static let shared = ImageCacheManager(countLimit: 100, totalCostLimit: 1024 * 1024 * 100)
    
    private var cache = NSCache<NSString, UIImage>()
    var cancelBag = Set<AnyCancellable>()

    private init(
        countLimit: Int,
        totalCostLimit: Int
    ) {
        super.init()
        cache.delegate = self
        cache.countLimit = countLimit
        cache.totalCostLimit = totalCostLimit
    }
}

최대 170MB

이미지의 크기가 너무 제각각이라 (원본을 저장합니다.) 일정하지 않은 메모리 사용량을 볼 수 있지만 200MB가 넘었을 때보다 적어진 사용량을 볼 수 있었습니다.

어느정도를 캐시 메모리로 잡아야할지는 이미지 크기를 일정하게 줄인 후 고려해보겠습니다.

 

삭제되는 이미지의 정보를 보았을 때 이미지를 원본으로 저장하기 때문에
이미지의 크기가 제각각이고 크기가 크다는 사실을 발견할 수 있었습니다.

 

2. 이미지의 크기

처음에는 홈화면의 썸네일 이미지와 디테일뷰의 이미지를 함께 생각해서 이미지의 크기를 어떻게 줄여야할지 고민했습니다.

하지만 디테일뷰는 웹뷰로 이미지를 함께 렌더링하기 때문에 (정확히 어떻게 불러오는지는 잘 모르겠지만 메모리가 크게 늘어나지 않아서)

UIImageView로 캐시로직을 고려할때 썸네일 이미지만 고려해서 이미지의 크기를 썸네일 이미지 크기로 줄이기로 결정했습니다.

 

킹피셔는 이 썸네일 이미지를 캐시하고 원본 이미지도 함께 저장하지만

버스트캠프는 원본이미지를 사실상 쓸 일이 없기 때문에 썸네일 이미지를 원본처럼 사용하기로 했습니다.

 

 

그러나 메모리 사용량을 줄이려면 resizing이 아니라 downSampling을 해야한다는 사실 .. !

 

간략히 이유를 적으면,

UIImage는 Data Buffer에서 Image Buffer로 디코딩하는 역할을 하는데

이 디코딩을 하는 작업은 비용이 굉장히 크기 때문에 이 버퍼 원본 사이즈를 줄여서 디코딩을 하면 메모리 사용량을 줄일 수 있습니다.

 

 

downsampling 으로 구현해보았습니다.

private func createThumnail(data: Data, size: CGSize) -> UIImage {
    guard let cgImageSource = CGImageSourceCreateWithData(data as CFData, nil)
    else { return UIImage() }

    let thumnailOptions = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height),
        kCGImageSourceCreateThumbnailWithTransform: true
    ] as CFDictionary

    guard let cgImage = CGImageSourceCreateThumbnailAtIndex(cgImageSource, 0, thumnailOptions)
    else { return UIImage() }

    return UIImage(cgImage: cgImage)
}

 

 

kCGImageSourceThumbnailMaxPixelSize 를 썸네일의 가로크기, 100으로 맞췄을 때

최대 90MB

썸네일의 크기가 100*75 라서 100으로 맞춰줬더니 이미지가 매우 흐릿하게 나오는 현상이 발생했습니다.

이미지 에셋을 줄 때 100, x2, x3으로 레티나 디스플레이를 생각해서 300으로 맞춰줬습니다.

 

 

 

kCGImageSourceThumbnailMaxPixelSize 를 썸네일의 가로크기, 300으로 맞췄을 때

최대 93MB

가끔 흐릿한 이미지가 있는데 어느정도 맞춰진 모습을 볼 수 있었습니다.

 

 

이미지를 다운샘플링하니 대략 60-70 MB의 메모리를 줄일 수 있었습니다.

(아직 NSCache 메모리는 10MB...ㅎㅎㅎ인데 왜 저렇게까지 올라가는 것인지..)

 

 

kCGImageSourceThumbnailMaxPixelSize   요놈..

https://developer.apple.com/documentation/imageio/kcgimagesourcethumbnailmaxpixelsize

 

Apple Developer Documentation

 

developer.apple.com

private func createThumnail(data: Data) -> UIImage {
    guard let cgImageSource = CGImageSourceCreateWithData(data as CFData, nil)
    else { return UIImage() }

    let thumnailOptions = [
        kCGImageSourceShouldCache: true,
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceThumbnailMaxPixelSize: 100 * 3, // 썸네일 이미지 100*75 (x3)
        kCGImageSourceCreateThumbnailWithTransform: true
    ] as CFDictionary

    guard let cgImage = CGImageSourceCreateThumbnailAtIndex(cgImageSource, 0, thumnailOptions)
    else { return UIImage() }

    return UIImage(cgImage: cgImage)
}

애플 문서, wwdc 예제 코드와 다른 사람들의 예제 코드를 보았을 때

모두 kCGImageSourceThumbnailMaxPixelSize 최대 픽셀 값을

UIImage를 받아서 지정해주거나 cgSize로 지정하거나 직접 값을 넣어주는 방식으로 구현하고 있었습니다만..

 

UIImage로 받는 것은 UIImage로 디코딩 되기 전에 다운샘플링을 하는 것이 목적인데 

UIImage를 받고 -> 다운 샘플링의 사이즈를 정해서 -> 다시 UIImage로 반환하는 것은

메모리를 줄이려는 목적과 일치하지 않는다고 생각해서 보류되었습니다..

 

그래서 직접 썸네일의 값 x3 = 300을 위에서 넣어주었는데 문제는 가로로 긴 이미지에 있었습니다.

300으로 줄여버리니 매우 깨져보이는 현상이 나타났습니다..ㅠㅠ 

문제는 가져오는 이미지의 사이즈가 정말 모두~~~~ 제각각 (작은거, 큰거, 가로로 긴 것, 세로로 긴 것) 다르다는 것이었습니다...

이게 왜 문제가 되냐면

저는 Data 형식을 받아서 cgImage를 생성해주기 때문에 Data 형식에서 image Size를 알 수 없다는 것이었습니다..

 

그래서 미친듯이 구글링한 결과 아래 사이트에서 힌트를 얻을 수 있었습니다.

 

https://nshipster.com/image-resizing/

 

Image Resizing Techniques

Since time immemorial, iOS developers have been perplexed by a singular question: ‘How do you resize an image?’ This article endeavors to provide a clear answer to this eternal question.

nshipster.com

Accelerate.vImage 를 import 하면 저 vImagePixelCount라는 타입이 있는데 

이 타입은 UInt를 typealias 한 것이라서 

저 모듈을 import 하지 않고 UInt로 형변환해서 가져올 수 있었습니다.

 

 

이제 이 사이즈를 어떻게 줄이나..

3분의 1로 줄여보고, 이미지 압축 알고리즘을 찾아봤지만..

정확하게 비율, aspectFill을 고려한 maxPixel값을 도저히 찾을 수 없어서

임의로 300 아래는 제 값으로,

600 아래는 300 값으로 고정,

600 위는 2분의 1 값으로 고정하기로 했습니다.

private func maxPixel(width: UInt, height: UInt) -> Int {
    let max = Int(max(width, height))
    print("최대픽셀", maxPixel)
    switch max {
    case 0...300: return max
    case 301...600: return ImageCacheManager.thumnailMaxPixel
    default: return max / 2
    }
}

 

결과는..!

 

원본
max pixel 300
커스텀 max pixel 적용

 

 

 

완벽한 원본 구현은 아니지만, 메모리를 줄이고 어느정도 원본을 유지한 채

메모리 사용량을 (NSCache 용량 100MB) 일정 수준으로 유지할 수 있었습니다.

 

 

 

 

 

디스크 캐시를 개선하는 방법은 다음 포스트에..

 

 

 

이미지 캐시

https://medium.com/@mshcheglov/reusable-image-cache-in-swift-9b90eb338e8d

 

Reusable Image Cache in Swift

Almost every application contains some kind of graphics. That is the reason why downloading and displaying images in a mobile application…

medium.com

https://jeonyeohun.tistory.com/367

 

[iOS] 메이트러너: 반쪽짜리 이미지 캐시 개선하기

메이트 러너 앱 개발 과정을 공유하는 포스트입니다! GitHub - boostcampwm-2021/iOS06-MateRunner: 함께 달리는 즐거움, Mate Runner 🏃🏻‍♂️🏃🏻‍♀️ 함께 달리는 즐거움, Mate Runner 🏃🏻‍♂️🏃🏻

jeonyeohun.tistory.com

다운샘플링

https://motosw3600.tistory.com/m/12

 

Image Optimizing

Image Memory Size 2048x1536 픽셀의 590KB size의 이미지를 앱에서 로딩하면 얼마의 메모리가 필요할까? 약 14MB(2048x1536x4/1000000), 픽셀당 4바이트 가정 매우 많은 메모리 소비 Image Rendring Process 1. Load(iOS는 압

motosw3600.tistory.com

https://velog.io/@dev_jane/UICollectionView-이미지-처리-downsampling

 

UICollectionView 이미지 처리: downsampling(feat. WWDC Image and Graphics Best Practices)

문제 상황 검색 결과를 UICollectionView에 표시할때 크기가 큰 이미지가 들어오는 상황에서 이미지 로딩 속도가 느리고, 메모리 사용량이 급격하게 늘어남 스크롤시 이미지가 깜빡거리면서 바뀌기

velog.io

https://hucet.tistory.com/41

 

[WWDC 2018] iOS Memory Deep Dive (2/2)

WWDC 2018 iOS Memory Deep Dive (1/2) WWDC 2018 iOS Memory Deep Dive (2/2) Images 이미지에서 메모리 사용은 파일 크기가 아니라 이미지의 크기와 관련이 있다는 것입니다. why is it so much larger? SRGB Format 픽셀 당 8 비

hucet.tistory.com

https://soooprmx.com/9670-2/

 

이미지 리사이즈 방법 총정리 - Swift · Wireframe

최근에 이미지 크기를 일괄적으로 줄여서 리사이징하는 간단한 도구를 만들어 봤는데, 요상하게 결과 이미지가 흐려졌다. 결과가 맘에 들지 않아서 좀 더 고품질의 결과를 얻는 방법을 찾기 위

soooprmx.com

다운샘플링 - imageIO

https://bastian.codes/blog/improving-image-rendering-using-coregraphics/

 

Improving image rendering using ImageIO · bastian.codes

Jul 15, 2019 Improving image rendering using ImageIO Displaying images has become standard practice for many categories of apps. But if images rendered in applications are not pre-processed properly, it can have a negativ impact on performance and therefor

bastian.codes

다운샘플링 vs UIImage

https://developer.apple.com/forums/thread/109445

 

DownSampling / Scaling Image Quali… | Apple Developer Forums

It's been a while since you asked this question, but I'm gonna try to answer it anyway: I think this is due to your image view being 300 points in size, and your image being 300 pixels wide. Points =/= Pixels on iOS. For Example, on standard Retina display

developer.apple.com

 

다운샘플링imgeIO