목차
- 이미지
- 이미지 처리 프로세스
- 문제점
- 이미지 리사이징
- 다운 샘플링
- ImageI/O
- NSURL
- 결과
이미지
가로 2048, 세로 1536 픽셀의 이미지가 있고 이 이미지 파일의 용량은 590KB입니다.
하지만 이 이미지가 iOS 앱에서 보여지기 위해 메모리에 올라갈 땐 얼마나 사용될까요?
무려 10MB 를 사용하게 됩니다..
왜 그런지 알아볼게요
이미지 처리 프로세스
이미지는 JPEG으로 압축되어있는 데이터 타입입니다.
이 이미지를 렌더링하는 과정은 다음과 같습니다.
- Load
- 데이터 타입의 이미지를 메모리에 load합니다.
- Decode
- 메모리의 이미지를 GPU가 읽을 수 있는 형태로 디코딩 합니다.
- 해당 과정에서 JPEG으로 압축된 파일을 압축 해제하며 10MB가 됩니다.
- Render
- 디코딩된 이미지를 렌더링하여 우리가 볼 수 있게 됩니다.
위 과정에서 디코딩과 이미지를 렌더링할 때 많은 메모리가 쓰이고 많은 CPU가 사용됩니다.
그리고 왜 디코딩 하니까 10MB가 되느냐?
예시 이미지는 렌더링 포맷이 SRGB 이므로 픽셀당 4byte
를 필요로 합니다.
4byte는 red, green, blue, alpha
각각 1byte씩 차지하고 있습니다.
다시 red, green, blue, alpha
는 각각 0~255
까지 표현할 수 있기 때문에 8bit 즉 1byte씩 차지하게 됩니다.
문제점
앱에서 이렇게 큰 이미지를 렌더링한다면 메모리 부족으로 앱이 죽어버리는 경험을 종종 하게 됩니다.
이런 문제를 해결하고자 이미지 사이즈를 줄이는 등 개발자는 문제를 해결하기 위해 노력합니다.
먼저 이미지 작업을 하기 위해 사용하는 방식은 2가지가 있습니다.UIGraphicsBeginImageContextWithOption
와 UIGraphicsImageRenderer
UIGraphicsBeginImageContextWithOption
는 이젠 Deprecated된 메소드로 iOS 4.0+, ARGB 32-bit 방식입니다.
UIGraphicsImageRenderer
는 iOS 10+, 최적의 렌더링 포맷을 자동으로 선택합니다.
이미지 리사이징
UIGraphicsImageRenderer
를 사용해 이미지를 리사이징하는 코드를 살펴보겠습니다.
extension UIImage {
func resize(newWidth: CGFloat) -> UIImage {
let scale = newWidth / self.size.width
let newHeight = self.size.height * scale
let newSize = CGSize(width: newWidth, height: newHeight)
let resized = UIGraphicsImageRenderer(size: newSize)
let resizedImage = resized.image { context in
self.draw(in: CGRect(origin: .zero, size: newSize))
}
printData(resizedImage)
return resizedImage
}
}
코드 내용은 간단합니다. 파라미터로 새로 적용할 가로 길이를 받고, 원본 비율을 유지하며 리사이징합니다. UIGraphicsImageRenderer
객체를 생성하고 image()
함수의 클로저를 통해 리사이징해줍니다.
원본 이미지는 2016 x 1512 사이즈의 3배수 이미지로, 52MB 인 반면
이미지뷰의 크기에 맞춰 리사이징 했더니 2MB로 줄은 걸 볼 수 있습니다.
그러나 아까 디코딩과 렌더링을 통해 이미지를 만들 때 많은 메모리와 CPU가 사용된다고 했었습니다.
let resizedImage = resized.image { context in
self.draw(in: CGRect(origin: .zero, size: newSize))
}
위 코드를 실행할 때 CPU와 메모리 사용량이 급격하게 오르는 것을 확인할 수 있었습니다.
큰 이미지를 사용하거나 여러 이미지를 보여주기 위해 반복적으로 렌더링을 하면 성능 이슈가 발생할 위험이 커보입니다.
그래서 WWDC에서 제안한 방법은 DownSampling을 통해 이미지 사이즈를 줄이는 방법을 제안합니다.
다운 샘플링
UIImage는 이미지를 리사이징하는 과정에서 많은 비용을 소모
한다고 합니다.
위에서 사용된 UIGraphicsImageRenderer
는 UIImage를 통해 리사이징을 진행합니다.
그렇기 때문에, 구식 API보단 효율적
이지만, 이미지의 크기를 바꾸는 과정에서는 비효율적
이라고 합니다.
그 이유를 다음과 같이 설명합니다.
- 이미지의 전체 정보를 압축 해제하여 메모리에 로드한다.
- UIImage 객체는 내부적으로 이미지의 크기, 위치 및 다른 속성들을 관리하기 위해 내부의 좌표 공간을 사용한다.
- 이미지를 회전, 크기 조정 또는 반전 등의 작업을 위해서는 이 좌표 공간들을 변환하는데에 비용이 많이 든다.
애플에서는 UIImage 수준에서 이미지를 다루는 것보다 효율적인 방법을 소개했습니다.
Image I/O
이미지 리사이징을 위한 가장 효율적인 방법으로는 Image I/O
를 사용하는 것입니다.
The Image I/O framework allows applications to read and write most image file formats.
This framework offers high efficiency, color management, and access to image metadata.
ImageIO는 프레임워크로 대부분의 이미지 파일 형식
을 읽고 쓸 수 있고,효율적
이면서 색상 관리
나 메타데이터 접근을 제공
합니다.
ImageIO는 UIImage가 아닌 CGImage
를 사용합니다. CGImage는 UIImage와 다르게 이미지의 기본적인 데이터만 가지고 있습니다.
import ImageIO
extension UIImage {
func downSample(scale: CGFloat) -> UIImage {
let data = self.pngData()! as CFData
let imageSource = CGImageSourceCreateWithData(data, nil)!
let maxPixel = max(self.size.width, self.size.height) * scale
let options = [
kCGImageSourceThumbnailMaxPixelSize: maxPixel,
kCGImageSourceCreateThumbnailFromImageAlways: true
] as CFDictionary
let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options)!
printData(UIImage(cgImage: scaledImage))
return UIImage(cgImage: scaledImage)
}
}
위 코드는 ImageIO를 통해 원하는 scale로 다운샘플링을 진행하는 코드입니다.kCG...maxPixelSize
는 width와 height 중 더 큰 값의 비율에 맞춰 나머지 값을 조절해준다고 합니다.
NSURL
let path = Bundle.main.path(forResource: "Coffee", ofType: "png")!
let fileUrl = NSURL(fileURLWithPath: path)
let imageSource = CGImageSourceCreateWithURL(fileUrl, nil)!
low level의 작업을 진행하기 때문에, URL보다 낮은 수준의 NSURL을 사용하고,CGImageSourceCreateWithURL
를 사용해 URL로 이미지를 불러올 수 있습니다.
결과
리사이징 시 2MB 였던 UIGraphicsImageRenderer
방식에 비해 용량이 더 줄었습니다.
참고 링크
https://developer.apple.com/videos/play/wwdc2018/416/
https://velog.io/@o_joon_/Swift-Image-and-Memory#downsampling
'iOS' 카테고리의 다른 글
GCD 공식문서 읽고 정리하기 (0) | 2024.07.26 |
---|---|
Swift 6.0 접근제어자 Access Control (0) | 2024.07.02 |
모듈화란? with Tuist (2) | 2024.06.03 |
SwiftData 알아보기 (1) | 2024.05.24 |
InjectionIII 라이브러리를 적용하면서 마주한 이슈들 (0) | 2024.05.02 |