[iOS] Image and Graphics Best Practices

MSDev·2022년 5월 15일
0

작년 핀더가든 앱을 개발하면서 이미지 로드, 다운샘플링과 캐싱에 kingfisher 라이브러리를 사용했습니다.

하지만 kingfisher 라이브러리를 사용하면서 iOS에서는 어떻게 이미지 캐싱을 하고 메모리 최적화를 위해 다운샘플링을 하는지 등 원리에 대해 이해하지 않고 사용법만 익히고 사용한 것 같아 iOS에서는 어떤 원리로 효율적인 image 처리를 할 수 있는지 알아보려고 합니다.

이번엔 UIImageView에 이미지가 렌더링되는 원리와 이미지 최적화 방법에 대한 WWDC를 보며 공부하고 정리해보려고 합니다.
Image and Graphics Best Practices라는 주제인데 링크는 아래 참고에 걸어두겠습니다 :)

먼저 Graphical contents를 작업하기 위한 High-Level tool인 UIImageUIImageView에 대해 얘기해보겠습니다.

UIImage는 하나의 데이터 타입으로 Image를 로드하는 역할을 하고, UIImageView는 이미지 표시와 렌더링을 담당하기 때문에 애플의 MVC 구조에서 단순히 UIImage는 Model의 역할을 하고 UIImageView는 View의 역할을 하는 관계라고 생각하실 수 있습니다.

하지만 화면에 이미지를 보여주는 과정은 위처럼 one-time event가 아니라 아래 사진과 같이 continuous한 process입니다. 또 사이 Decoding 단계도 포함되어 있습니다.

Decode를 설명하기 전에! Buffer에 대해 알아야합니다.
Buffer는 단순히 메모리의 연속적인 영역으로 같은 element들의 sequence로 구성된 메모리로 표시됩니다. 그 중 Image Buffer가 이번에 주요하게 봐야할 Buffer 중 하나입니다.

Image Buffer는 이미지의 메모리 내 표현을 가지고 있는 Buffer입니다.
각 element들은 픽셀 하나의 색과 투명도를 표시하기 때문에 해당 Buffer size는 가지고 있는 image size에 비례합니다.

그 중 Frame Buffer라는 것은 실제 어플리케이션의 rendering된 결과를 보관하는 buffer입니다.

위 사진을 보면서 이미지가 화면에 표시되는 전체 과정을 얘기해보면
먼저 어플리케이션이 view hierarchy를 변경하면
UIKit은 어플리케이션의 window와 모든 subview들을 frame buffer로 rendering합니다.
그 후 frame buffer는 하드웨어에 보여줄 각 픽셀 색상 정보를 제공하고 하드웨어에서 frame buffer를 통해 가져온 컨텐츠를 노출시킵니다.

이 때 60~120fps로 fixed interval(1/120~1/60초)을 가집니다. 위에서 말했듯이 화면에 이미지를 보여주는 과정은 주기적으로 화면을 rendering하는데 해당 interval로 항상 re-rendering하는 것은 아니고 frame buffer를 interval마다 조회하고 변경된 내용이 있는 경우 re-rendering을 합니다.
따라서 어플리케이션에 변화가 없다면 하드웨어는 frame buffer로부터 같은 데이터를 가져와 이전과 같은 화면을 보여줄 것 입니다.

하지만! 어플리케이션의 view content를 변경한다면! 아래 사진을 예로 UIImageView에 새로운 UIImage가 들어온다면 UIKit은 어플리케이션의 window를 frame buffer에 re-render 시키고 하드웨어는 frame buffer에서 새로운 컨텐츠를 노출시키게 됩니다.

다음으로 또 다른 Image Buffer인 Data Buffer에 대해 얘기해보겠습니다.
Data Buffer는 bytes의 sequence를 포함하는 buffer로 이미지에 대한 Metadata와 JPEG 또는 PNG 형태로 encode된 이미지 데이터 자체가 저장됩니다. 여기서 Metadata의 byte들은 이미지 픽셀에 대한 정보는 가지고 있지 않습니다.

이제는! Image data와 UIImage, UIImageView를 통해 화면에 이미지를 표시하는 과정을 살펴봅시다.

먼저 위의 사진을 보면서 크게 살펴보겠습니다.
네트워크를 통해 다운받았거나 disk에서 읽어온 이미지 파일이 data buffer에 담겨있습니다.
저희는 각 픽셀에 대한 데이터를 해당 frame buffer에 제공해줘야합니다. 하지만 data buffer에는 각 픽셀에 대한 정보가 담겨져 있지 않죠. 여기서 decoding 개념이 나오게 됩니다.

그래서! UIImage는 data buffer에 담겨져 있는 이미지의 크기와 같은 image buffer를 할당해주고
JPEG이나 PNG 등 다른 encode된 이미지 데이터를 각 픽셀 이미지 정보로 바꾸어주는 decoding을 해줍니다.
그리고 UIImageView의 content mode에 따라 image buffer에서 frame buffer로 copy할 때 UIKit은 imageView에 image buffer에 있는 이미지 데이터를 copy하고 scale하여 크기를 조정합니다.

그러나 이 Decoding 단계는 특히나 사이즈가 큰 이미지에서 CPU-intensive process입니다.

따라서 UIKit이 계속 image view에게 rendering하도록 요청하는 것이 아니라 UIImage가 해당 image buffer를 계속 가지고 있어 한 번만 해당 작업이 일어나도록 합니다. 또, decoding된 이미지 데이터가 image buffer에 보관되기 때문에 decode되는 모든 이미지에 대해 영구적이고 큰 메모리 할당이 필요해질 수 있습니다. 이 때, 메모리 크기는 이미지 크기에 비례하지만 frame buffer에 실제 랜더링될 이미지 뷰의 크기에 비례할 필요는 없습니다.

그렇게 많은 메모리 사용량이 축적되면 OS는 물리적 메모리 영역을 압축하기 시작합니다. 이 단계에 CPU가 관여하고, 어플리케이션 내부의 CPU 사용량과 별개로 CPU 사용량이 증가하게 됩니다. 결국 어플리케이션은 너무 많은 물리적 메모리를 소모하게 되고, OS가 프로세스를 종료시켜야 하는 순간이 올 수 있습니다.

여기서! 어플리케이션에서 사용하는 메모리 양을 줄이기 위해 Downsampling을 사용할 수 있습니다.

[참고]

profile
iOS 개발자

0개의 댓글