일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- swiftUI
- collectionview
- ribs
- uitableview
- clean architecture
- UITextView
- 리펙토링
- Xcode
- 리펙터링
- swift documentation
- UICollectionView
- 스위프트
- Clean Code
- Observable
- 리팩토링
- MVVM
- uiscrollview
- ios
- 애니메이션
- Human interface guide
- 클린 코드
- map
- Refactoring
- rxswift
- combine
- SWIFT
- tableView
- Protocol
- HIG
- RxCocoa
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 7. Memory Deep Dive - 이미지 로드 매커니즘, 이미지 핸들링 최적화 (UIGraphicsBeginImageContextWithOptions, UIGraphicsImageRenderer) 본문
[iOS - swift] 7. Memory Deep Dive - 이미지 로드 매커니즘, 이미지 핸들링 최적화 (UIGraphicsBeginImageContextWithOptions, UIGraphicsImageRenderer)
jake-kim 2023. 12. 14. 01:37
1. Memory Deep Dive - iOS 메모리 운영체제 기초 (가상 메모리, 페이징, clean memory, dirty memory, compressed memory)
2. Memory Deep Dive - Memory를 줄여야 하는 이유 (+ 앱 메모리 사용량 아는 방법)
3. Memory Deep Dive - Memory Footprint (페이징, Compressed 메모리)
4. Memory Deep Dive - Memory Footprint 프로파일링 방법 (Allocation, Leaks, VM Tracker, Virtual memory trace)
5. Memory Deep Dive - Memory Footprint 프로파일링 방법 (2) (Xcode Memory Debugger, Memory Graph, Memgraph)
6. Memory Deep Dive - Memory Footprint 프로파일링 방법 (3) (vmmap, leaks, heap, malloc_history)
7. Memory Deep Dive - 이미지 로드 매커니즘, 이미지 핸들링 최적화 (UIGraphicsBeginImageContextWithOptions, UIGraphicsImageRenderer)
8. Memory Deep Dive - 이미지 리사이징, 이미지 다운 샘플링 (ImageIO, ImageSource)
9. Memory Deep Dive - 백그라운드에서 메모리 최적화하는 방법
이미지 메모리
- 이미지를 다룰때 중요한 것은 파일의 크기(volume)가 아닌 이미지의 크기(resolution)이라는 점을 알 것
- 이미지를 구성하고 있는 pixcel관점에서, 1pixel을 이룰 때 RGB요소에 의해 각 1byte씩 3개가 필요하므로 3byte가 필요
- 여기에다 alpha 채널까지 합하면 1pixel당 4byte가 필요
- 만약 크기가 2048px * 1536px 의 이미지 파일 크기가 590KB가 디스크에 있을 때, 이 파일을 뷰에 표현할때는 약 10mb가 필요 (2048px * 1536px * 4byte)
- 왜 뷰에 이미지를 적용할 때 파일에 있는것보다 더욱 크게 보여주는지? (아래에서 계속)
iOS에서 이미지가 어떻게 작동하는지 이해하기
- 1단계) load
- 압축된 590kb 파일 jpg를 메모리에 로드
- 2단계) decode
- jpg를 GPU가 읽을 수 있는 형식으로 변환
- 이 단계에서 압축을 풀게되어 10mb로 크기가 증가
- (디코딩되면 마음대로 렌더링이 가능)
- 3단계) render
- 준비된 10mb파일을 뷰에 그리는 작업
이미지 포멧의 용량
- 1) wide format 이미지
- 일반 이미지 포멧은 1pixel 당 4byte이지만 wide format이미지는 8btye
- iOS에서는 wide format의 이미지를 렌더링할 수 있는데, 이 wide format은 픽셀을 2배 늘린 것이고 RGB(with alpha) 각 2byte가 필요하므로 1pixel당 총 8btye가 필요
- 2) luminance and alpha 8 format 이미지
- * luminance: 휘도 (눈부심 정도)
- 회색조와 알파 값만 저장하므로 픽셀당 2byte만 필요
- metal앱과 shader에서 사용
- 3) alpha 8 format 이미지
- 1pixel당 1byte만 필요한 포멧이며 sRGB보다 75% 더 작음
- 흑백의 mask나 텍스트에 사용
- 즉 이미지의 1pixel 당 크기는 1byte에서 8byte까지 사용
- 그렇다면 어떤 기준으로 이런 format을 선택해야하는가?
- format을 일일이 선택하는 것보다, 애플에서 제공하는 이미지 렌더링 API를 잘 사용하는것이 중요
이미지 렌더링 API
- 1) UIGraphicsBeginImageContextWithOptions
- iOS가 만들어기 초창기부터 있었고 이것을 사용하면 이미지는 1pixel당 8bye가 잡혀 있었기 때문에 메모리 공간을 매우 비효율적으로 사용
- 이 API는 되도록 사용하지 말것
- 2) UIGraphicsImageRenderer
- iOS 10부터 사용
- 이 방식은 BeginImageContextWithOptions가 항상 1pixel당 4byte를 사용하도록 되어있기 때문에 1번 방법보다 더 많은 메모리 절약이 가능
- iOS 10, iOS11 에서 이 방법을 사용하면 8byte를 사용하는 wide format 이미지를 얻을 수 없음
- iOS 12부터는 내부적으로 알아서 graphics format을 사용하도록하여 이미지가 wide format인 경우 자동으로 8btye를 사용하도록 수정됨
ex) UIGraphicsBeginImageContextWithOptions와 UIGraphicsImageRender 코드 비교
- UIGraphicsBeginImageContextWithOptions를 사용하면 1pixel당 4btye 모두를 사용
func drawImageV1() {
let bounds = CGRect(x: 0, y: 0, width: 300, height: 100)
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)
/// drawing 코드 start
UIColor.black.setFill()
let path = UIBezierPath(
roundedRect: bounds,
byRoundingCorners: UIRectCorner.allCorners,
cornerRadii: CGSize(width: 20, height: 20)
)
path.addClip()
UIRectFill(bounds)
/// drawing 코드 end
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageView.image = image
}
- 만약 UIGraphicsImageRender를 사용하면 decode 단계에서 1pixel당 1byte만 사용하도록 표현이 가능
- 1pixel당 1btye만 메모리에 할당해놓고 그려준 후, imageView에 color정보를 적용하는 방식
- 메모리 공간이 75% 절약하여 사용이 가능
- 추가 메모리 비용 없이 파란색, 빨간색, 녹색 모두 사용이 가능
func drawImageV2() {
let bounds = CGRect(x: 0, y: 0, width: 300, height: 100)
let renderer = UIGraphicsImageRenderer(size: bounds.size) /// <-
/// drawing 코드
let image = renderer.image { context in
UIColor.black.setFill()
let path = UIBezierPath(
roundedRect: bounds,
byRoundingCorners: UIRectCorner.allCorners,
cornerRadii: CGSize(width: 20, height: 20)
)
path.addClip()
UIRectFill(bounds)
}
imageView.image = image
imageView.tintColor = .black // <-
}
- 큰 이미지를 다룰 때 메모리로 올려야 하는경우, 이미지 다운 샘플링을 통하여 메모리 최적화가 가능한데, 이 방법은 다음 포스팅 글에서 계속...
* 전체 코드: https://github.com/JK0369/ExImageMemory
* 참고