관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 7. Memory Deep Dive - 이미지 로드 매커니즘, 이미지 핸들링 최적화 (UIGraphicsBeginImageContextWithOptions, UIGraphicsImageRenderer) 본문

iOS 응용 (swift)

[iOS - swift] 7. Memory Deep Dive - 이미지 로드 매커니즘, 이미지 핸들링 최적화 (UIGraphicsBeginImageContextWithOptions, UIGraphicsImageRenderer)

jake-kim 2023. 12. 14. 01:37

* 가장 기초) iOS 메모리 기초 개념 - virtual memory, dirty memory, clean memory, compressed memory, swapped memory 이해하기 포스팅 글

 

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파일을 뷰에 그리는 작업

디스크에 있는 파일을 읽어서 뷰에 그림을 그리기 까지 3단계 과정

이미지 포멧의 용량

  • 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

* 참고

- https://developer.apple.com/videos/play/wwdc2018/416/

Comments