iOS

[iOS] WWDC 2018 Image and Graphics Best Practice

felix-mr 2021. 7. 21. 19:26

안녕하세요 🙇‍♂️

이번 게시글은 WWDC 2018 Image and Graphics Best Practice 에 대해 작성해보려고 합니다.

Medium Daily Digest를 구독하고 있는데 관련글을 보다가 찾아보게 되었는데

그 동안 Image를 잘못 다루고 있었구나라는 반성을 했습니다..😭

 

자자 일단 바로 시작해봅시다!

 

이 영상에서는 App에서 Graphical Contents들을 효율적으로 사용하기 위한 기술과 전략에 대해 공유하는 영상입니다.

 

먼저 App에서 Graphical Contents를 작업하기 위한 고급 도구(High-Level Tool)인 UIImage와 UIImageView에 대해 알아보겠습니다!

 

Graphical Contents 는

  1. 다양한 정보를 가지고 있는 사진 (rich content photograph)
  2. 아이콘 (iconography)

요런식으로 두가지 카테고리로 분류 될 수 있겠죠?

 

UIImage는 UIKit에서 이 버튼에 표시되는 아이콘 같은 것을 나타낼 때 사용하는 데이터 유형입니다.

 

UIImageView는 UIImage를 표시하기 위해 UIKit에서 제공하는 클래스입니다.

또, 이름에서 알 수 있듯이 View입니다.

 

 

일반적으로 이렇게 많이들 쓰죠?

 

따라서,

 

UIImage는 Image를 로드하는 역할을 하고, UIImageView는 이미지 표시와 렌더링을 담당하기 때문에

UIImage는 모델의 역할을 하고 UIImageView는 뷰의 역할을 하니까 그냥 단순한 관계겠네?

라고 생각할 수 있지만!

 

일단 렌더링 단계는 지속적으로 이루어지는 프로세스일 뿐만아니라

 

Decoding 단계 또한 숨겨져 있습니다

 

자 하지만! 디코딩 단계를 알기 전에 먼저 버퍼에 대해서 알아봐야 될 것 같아유😅

 

버퍼는 여러가지 의미로 사용되지만 여기서 얘기하는 버퍼는

 

일부 이미지의 메모리 내 표현을 보관하는

이미지 버퍼 입니다.

 

이미지 버퍼의 각 요소들은 이미지에서 픽셀 하나의의 색상과 투명도를 가지고 있습니다.

따라서 메모리에 있는 이 버퍼의 크기는 포함된 이미지의 크기에 비례합니다.

 

프레임 버퍼라는 것이 있습니다.

이 프레임 버퍼는 실제 렌더링 되어 출력된 데이터를 보관하는 버퍼입니다.

 

 

 

번역을 하면서 이해해보려고 하고는 있는데...... 무슨 소리죠 ㅎㅎ😂

 

 

 

 

자자!일반적으로 Image Data와 UIImage, UIImageView를 통해 화면에 Image를 표시하는 과정을 통해 이해해봅시다!

프레임 버퍼는 UIImageView를 실제로 화면에 Display 하는 과정에서(Rendering) 채워지는 영역을 말하는 것 같습니다.

(틀린 것 같으면 댓글 부탁드립니다.🤥)

 

그리고 UIImageView에 UIImage를 할당해야겠죠?

 

또, UI이미지를 생성하기 위해서는 네트워크에서 다운로드하거나 디스크에서 읽어오는 Data Buffer가 필요할 것입니다.

Data Buffer는 이미지 파일의 내용을 나타냅니다.

 

Data Buffer 자체를 Frame Buffer에 적용할 수 없기 때문에

위에서 얘기한 것 처럼 픽셀의 색상과 투명도를 포함한 Image Buffer로 변경해줘야 합니다.

이 때 우리는 Decode를 사용합니다.

 

그쵸? 일반적으로 Data를 디코딩해서 UIImage를 만들잖아요? 허허뭔가 익숙하면서 어려운 그런 느낌이네요.🤪

 

먼저 UIImage는 Decoding을 통해 Data Buffer에 포함된 이미지 크기와 동일한 Image Buffer를 할당합니다.

그리고 나서 UIImageView의 contentMode에 맞게 Decoding 작업을 실행합니다.

 

마지막으로 UIKit가 UIImageView를 Rendering하도록 요청하면 Image Buffer안에 저장되어 있는 이미지 데이터(색상과 투명도)를 Frame Buffer에 복사하고 크기를 조정합니다.

 

위의 과정들은 메모리를 메모리 할당이 지속적으로 발생하고, CPU를 많이 사용합니다. (can be CPU intensive.)

 

이미지를 불러올 때 Image Buffer와 Frame Buffer는 인메모리에 저장되어야 하기 때문에 순간적이거나 영구적으로 메모리 사용량이 증가할 것입니다.

 

App에서 가장 중요한 점 중 하나는 "한정된 자원을 어떻게 효율적으로 사용하냐" 입니다.

 

메모리를 어느 한도까지 사용하게 되면 App 자체가 종료되는 것은 다들 알고 계실 겁니다.메모리 자원을 효율적으로 사용하는 것은 굉장히 중요한 부분이죠?

 

자! 메모리의 중요성을 각인했으니까 다시 위의 과정들을 살펴봅시다.

실제로 Image Buffer가 Frame Buffer에 복사될 때 모든 픽셀을 사용하는 것은 아닙니다. Image View의 사이즈나 스케일 등에 영향을 받아서 필요한 픽셀의 정보만 복사되는 것을 볼 수 있습니다.

 

그렇기 때문에 우리는 디코딩 부분에서 메모리 사용량을 줄여볼 수 있을 것입니다.여기서 소개하는 방법이 바로 DownSampling 입니다.

 

그림을 보면서 알아보겠습니다.

위에서 말했듯이 Rendering 단계에서 이미지를 축소하는 것은 CoreAnimation이 담당합니다.우리가 사용할 DownSampling은 Decode 단계(Image Buffer 생성) 이전에 실행됩니다.

 

여기서 DownSampling 작업을 Thumbnail 객체로 캡쳐할 것입니다.

 

자 두번째 과정에 Thumbnail 객체를 통해 이미지를 축소하는 것을 확인할 수 있죠?이렇게 되면 디코딩되는 객체 자체가 작아지기 때문에 메모리의 총 사용량 또한 적어지게 될 것입니다!

 

🎉🎉🎉

 

이제 이 DownSampling 과정을 코드로 알아보도록 하겠습니다.

func downSampling(imageAt 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 downSamplingOptions = [
    kCGImageSourceCreateThumbnailFromImageAlways: true,
    kCGImageSourceShouldCacheImmediately: true,
    kCGImageSourceCreateThumbnailWithTransform: true,
    kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
  ] as CFDictionary

  let downSampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downSamplingOptions)!

  return UIImage(cgImage: downSampledImage)
}

 

low-level에서 다뤄줘야하는 부분들이기 때문에 생소한 코드들을 보실 수 있습니다.

차근차근 알아보면서 이해해봅시다.

 

 

let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!

CGImageSource를 만들어주기 위해서 먼저 imageSoruceOptions를 설정해줍니다.

kCGImageSourceShouldCache이미지를 디코딩된 형식으로 캐시할지 여부입니다.

 

우리는 바로 디코딩된 형식이 필요한 것이 아니라 사이즈를 줄여야 되는 것이 우선이기 때문에 Caching 작업을 진행하지 않을 것입니다.

kCGImageSourceShouldCache: false 를 적용해줍니다.

그리고 생성한 옵션을 통해 imageSource를 생성해줍시다!

 

let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale

그리고 스케일과 Rendering할 포인트 크기에 맞춰서 생성될 Thumbnail의 최대 width 또는 height를 계산해줍니다.

Rendering할 포인트 크기란 UIImageView의 사이즈라고 이해하면 되겠죠?

 

let downSamplingOptions = [
  kCGImageSourceCreateThumbnailFromImageAlways: true,
  kCGImageSourceShouldCacheImmediately: true,
  kCGImageSourceCreateThumbnailWithTransform: true,
  kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary

이제 DownSampling을 진행할 때 필요한 옵션들을 설정해줍니다.

 

친절하게도 직접 document 찾아서 알아보라고 해주시네요! ㅎㅎ

 

자 하나씩 찾아서 알아봅시다!

 

kCGImageSourceCreateThumbnailFromImageAlways

이미지 원본 파일에 썸네일이 있더라도 전체 이미지에서 썸네일을 만들어야 하는지 여부입니다.
썸네일은 전체 이미지로부터 생성되며, kCGImageSourceThumbnailMaxPixelSize에 지정된 크기 제한에 따릅니다.
kCGImageSourceThumbnailMaxPixelSize를 지정하지 않은 경우 썸네일의 크기는 전체 이미지의 크기가 됩니다.
이 크기는 사용자가 원하는 크기가 아닐 수 있습니다. 기본값은 false입니다.
이 Key는 CGImageSourceCreateThumbnailAtIndex(_:_:_:) 에 전달될 옵션 Dictionary에서 사용됩니다.

 

 

kCGImageSourceShouldCacheImmediately (중요!)

오! 공식문서에 아무것도 적혀있지 않네요! ㅎㅎ🤯

다른 자료들을 찾아본 것을 바탕으로 정리해보겠습니다!

 

공식적인 내용이 아니기 때문에 인용문은 사용하지 않겠습니다.

 

이 옵션은 CGImageSourceCreateThumbnailAtIndex(_:_:_:) 을 통해 썸네일을 생성할 때 이미지를 디코딩함으로써 Image Buffer를 만들지에 대한 옵션입니다.

 

이 부분이 중요한 이유는 우리가 ImageSource를 생성할 때 kCGImageSourceShouldCache 옵션을 통해서 Cache를 하지 않겠다고 했잖아요?

 

왜냐면 그 때는 버퍼 메모리가 컸기 때문이죠?

 

하지만 썸네일을 생성하는 시점에는 버퍼의 메모리를 줄였기 때문에 이 시점에서 디코딩을 하겠다는 의미입니다.

kCGImageSourceShouldCache와 kCGImageSourceShouldCacheImmediately 이 두 옵션을 통해 CPU가 디코딩하는 타이밍을 결정할 수 있습니다!

 

중요한 부분이기 때문에 참 어려운 것 같습니다.😭

 

kCGImageSourceCreateThumbnailWithTransform

전체 이미지의 방향 및 픽셀 가로 세로 비율에 따라 썸네일을 회전하고 축척(scale)해야 하는지 여부입니다.

 

kCGImageSourceThumbnailMaxPixelSize

썸네일의 최대 너비 및 높이(픽셀)를 지정합니다.
이 키를 지정하지 않으면 썸네일의 폭과 높이가 제한되지 않으며 썸네일이 이미지 자체만큼 커질 수 있습니다.

우리가 DownSampling하는 목적은 썸네일을 통해 용량을 줄이기 위함이죠?

이 부분도 지정하지 않으면 이미지 그 자체만큼 커질 수 있기 때문에 꼭 지정해줘야할 중요한 Key인 것 같습니다.

 

let downSampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downSamplingOptions)!
return UIImage(cgImage: downSampledImage)

그리고 마지막으로 CGImage 형태의 썸네일을 생성한후에 UIImage 형태로 반환을 해줍니다.

 

CGImage를 생성해 줄 때 index를 설정해줘야 하는 이유는 CGImageSource 자체가 여러 이미지, 썸네일 이미지를 포함하고 있기 때문입니다.

 

그럼 이렇게 생성된 이미지가 실제로 어느정도의 메모리 사용량을 감소 시킬 수 있는지 확인해봅시다.

오오옹!

 

엄청나요!!

 

왜 여태 이걸 몰랐었나 반성합니다...😥

 

꼭 사용해야할 기술인 것 같습니다. 여러분들도 꼭 사용하세요!

 

WWDC 열심히 보겠습니다!

 

혹시 틀린 내용이 있거나 이해가 안되시는 부분은 댓글을 통해 말씀해주세요!

 

감사합니다 :)

 

 

 

Reference

https://developer.apple.com/videos/play/wwdc2018/219/?time=877 

 

Image and Graphics Best Practices - WWDC18 - Videos - Apple Developer

Whether it's for UI elements or a fundamental part of your application, at some point, you have to handle images. This session is packed...

developer.apple.com