Danny의 iOS 컨닝페이퍼
article thumbnail

[TIL #20] 2023 / 05 / 01

앨범에서 이미지를 불러오는 작업을 하는데, 사진을 추가할 때마다 메모리 사용량이 기하급수적으로 증가하더라고요.

 

그래서 오늘은 간단히 이미지 관련, 메모리 최적화 방법에 대해서 알아보려고 합니다.

 

WWDC 18(iOS Memory Deep Dive) 와 WWDC18(Image and Graphics Best Practices) 를 참고해

정리하였습니다.

 

메모리 사용량은 이미지의 파일의 크기로 계산되는 게 아닙니다.

메모리 사용량 ≠ 파일의 크기

 

바로 이미지의 크기(해상도)로 메모리가 계산이 됩니다.

 

계산 공식은 이와 같습니다. (Assets @1x 크기 기준)

메모리 사용량 ≈ 해상도(가로 pixels * 세로 pixels) * 4byte per pixel

 

대략적인, 메모리 사용량은 코드로 이렇게 구할 수 있겠죠!? 

func memoryUsage(for image: UIImage) {
    let imageSize = image.size
    let totalBytes = imageSize.width * imageSize.height * 4
    
    print("Memory used by the Image: \(totalBytes / pow(1024, 2)) MB")
}

let image = UIImage(named: "photo")!
memoryUsage(for: image)
// Memory usage for Image: 46.51171875 MB

 

먼저, iOS에서 이미지 처리 파이프라인(프로세스)을 간단히 살펴보면

 

Load

압축된 크기의 이미지 파일을 메모리에 로드시킵니다.

 

Decode

예를 들어 JPEG파일을 로드시켜 줬다면, GPU에서 읽을 수 있게 디코딩 작업이 실행됩니다.

여기는 데이터의 압축을 해제하여 이미지로 표현하므로 많은 메모리가 공간이 필요하게 됩니다.

(Data Buffer -> 디코딩 -> Image Buffer로 변환)

 

Render

매 초마다 화면에 업데이트되어 보여집니다.

(60Hz, 즉 매 초 60회 번씩 그려줍니다. iPad pro 시리즈는 120Hz도 지원합니다.)

 

그림을 보면서 조금 더 자세히 설명하면

만약, UIKit의 ImageView에서 렌더링을 요청하면, Decoding으로 변환된 Image Buffer 데이터를 복사해,

보여지는 영역의 데이터(Frame Buffer) 만큼 ImageView의 크기에 맞춰서 조정하는 작업을 하게 됩니다. 

 

 

다음으로, Image Render Format의 종류에는 여러 가지가 있는데, 기본 형식(SRGB)만 알고 갑시다.

이미지의 기본 렌더링 형식입니다 (red, green, blue, alpha 각 1byte, 한 픽셀 당 4byte입니다.)

그렇기 때문에 위의 메모리 사용량을 구할 때, 4 btye를 곱해서 구해준 거죠!

 

 

사용법

WWDC에서는 두 가지 메모리 최적화 방법을 소개해줬습니다.

 

1. UIGraphicsImageRenderer

2. downsampling

 

 

UIGraphicsImageRenderer

자동으로 최적의 Image Render Format을 선택해 줍니다.

최적의 Format을 자동으로 찾아 계산해 주므로, 이론상으로 75% 이상 메모리 절약 효과가 있다고 하네요.

 

메모리 사용량은 이미지의 해상도에 따라 영향을 받는다고 바로 위에서 말씀드렸어요.

 

그렇다면, UIGraphicsImageRenderer를 통해 이미지의 크기를 조절(resize)하게 되면,

자연스럽게 메모리 사용량이 줄겠죠?

 

 바로 사용 방법을 알아봅시다.

 

 

기본 사용방법

let image = UIImage(named: "photo")!
let imageSize: CGSize = imageView.frame.size

let render = UIGraphicsImageRenderer(size: imageSize)
let renderImage = render.image { _ in
    image.draw(in: CGRect(origin: .zero, size: imageSize))
}

 

 

Extenstion 이용방법

extension UIImage {
   
    // 불러온 이미지 사이즈 변경 (Compact 버전)
    func resized(to size: CGSize) -> UIImage {
        let imageSize = size
        return UIGraphicsImageRenderer(size: imageSize).image { _ in
            draw(in: CGRect(origin: .zero, size: imageSize))
        }
    }
}

let image = UIImage(named: "photo")!
let imageSize: CGSize = imageView.frame.size
let renderImage = image.resized(to: imageSize)

 

그런데 이 방법은 문제점이 존재합니다. 일단 draw 동작은 많은 CPU 비용을 사용하게 됩니다.

 

그러므로 큰 해상도의 이미지를 사용하거나, 다시 그려질 때, 앱 성능에 문제가 발생할 수 있습니다.

 

그리고 만약, 이미지의 크기를 조절했지만,

보이는 화면과 이미지의 해상도가 같을 때는 메모리 사용량은 동일하므로 효과가 없겠죠?!

 

그래서, 대안으로 downsampling이라는 방법을 소개를 해줍니다.

 

 

Downsampling

위에서도 설명했지만 기본적으로 이미지 프로세서는 이렇습니다.

 

이미지가 로드되면, Data Buffer가 먼저 로드되고,

Decoding 된 데이터(Image Buffer)가 복사되어, 보여지는 데이터(Frame Buffer) 만큼 크기를 조절해 줍니다.

 

GPU에서 이미지 데이터를 읽는 Decoding 과정에서 메모리가 많이 사용된다고 했습니다.

 

만약, 필요한 크기만큼 데이터를 미리 축소한 뒤, 썸네일로 캡처하여 불필요한 Data Buffer를 제거한 채로

Decoding 작업을 하게 되면, 메모리를 절약할 수 있겠죠.

 

즉, Decoding에서 할 작업을 줄여 메모리 비용 낮춥니다.

(필요 없는 데이터를 사용하지 않으므로 메모리 절약)

그리고 Low-Level로 접근하여 사용하므로 처리 속도가 빠르다네요.

 

내용은 대략적으로 이렇습니다.

 

코드를 살펴봅시다. WWDC에서 소개된 코드입니다.

func downsampleImage(at imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
    let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
    let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!
    
    let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
    let downsampleOptions = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceShouldCacheImmediately: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
    ] as CFDictionary
    
    let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
    return UIImage(cgImage: downsampledImage)
}

 

그런데 정말... 하나도 모르겠네요 ㅎㅎ 공식문서를 참고해서 간단히 설명하겠습니다.


kCGImageSourceShouldCache

디코딩된 이미지를 캐시(임시 저장)할지 여부를 나타내는 값입니다.

여기선 당연히 디코딩되기 전 값이 필요하므로 false를 해야겠죠.

(참고로 기본값으로 true를 갖고 64bit, false일 경우 32bit 라네요)

 

 

CGImageSourceCreateWithURL

URL로 지정된 위치에서 읽는 이미지 소스를 만듭니다.

또한 Data를 사용한 CGImageSourceCreateWithData 메서드도 존재합니다.


downsampleOptions의 값들

 

kCGImageSourceCreateThumbnailFromImageAlways

항상 축소판 이미지를 만들지 여부를 나타냅니다.

(기본값을 false입니다)

 

 

kCGImageSourceShouldCacheImmediately

이미지 생성 시 이미지 디코딩 및 캐싱이 발생하는지 여부를 나타내는 부울 값입니다.

WWDC에서는 가장 중요한 옵션이라고 강조하네요.

자세히는 모르겠지만 썸네일이 생성되는 시점에 디코딩을 시작하도록 도와주는 것 같습니다.

(썸네일이 생성되는 시점에 CPU 동작을 하도록 컨트롤하는 건가 봅니다!?)

 

 

kCGImageSourceCreateThumbnailWithTransform

이미지의 방향 및 종횡비와 일치하도록 썸네일 이미지를 회전 및 크기를 조정할지 여부를 나타내는 부울 값입니다.

 

 

kCGImageSourceThumbnailMaxPixelSize

축소판 이미지의 최대 너비와 높이(픽셀 단위)입니다.

이 값을 설정해주지 않으면 썸네일이 원본 이미지만큼 커질 수 있다고 합니다.

 

 

CGImageSourceCreateThumbnailAtIndex

이미지 소스와 옵션을 갖고 지정된 인덱스에 이미지의 썸네일을 만들어 cgImage로 리턴하게 됩니다.

그 후 UIImage(cgImage:)로 이미지를 생성해서 사용


사용 방법

Downsampling은 이미지 경로를 찾아가므로, 당연히 URL로 접근해서 사용해야겠죠?

let imageSize: CGSize = imageView.frame.size

let url = Bundle.main.url(forResource: "photo", withExtension: "jpg")!
let renderImage = downsampleImage(at: url, to: imageSize, scale: 1)

imageView.image = renderImage

 

 

이렇게, 데이터도 전달받아 사용이 가능합니다.

func downsampleImage(at imageData: Data, to pointSize: CGSize, scale: CGFloat) -> UIImage {
    let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
    let imageSource = CGImageSourceCreateWithData(imageData as CFData, imageSourceOptions)!
    
    let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
    let downsampleOptions = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceShouldCacheImmediately: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
    ] as CFDictionary
    
    let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
    return UIImage(cgImage: downsampledImage)
}
let image = UIImage(named: "photo")!
let imageSize: CGSize = imageView.frame.size

let data = image.jpegData(compressionQuality: 1)!
let renderImage = downsampleImage(at: data, to: imageSize, scale: 1)

self.imageView.image = renderImage

 

 

마무리

UICollectionViewDataSourcePrefetching를 사용하여 CPU 점유율을 낮추는 방법도 소개해주는데...

아직 사용방법을 몰라서 이건 나중에 추가해서 올리도록 하겠습니다.

 

 

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!