HCN DEV

iOS Memory Footprint 분석 방법

iOS Memory Footprint 분석 방법 feature image

본 글은 2018년 WWDC의 iOS Memory Deep Dive 세션을 정리한 글입니다. 내용을 위주로 정리하였으며, 데모 부분은 별도로 정리하지 않았습니다. 전체 데모를 확인하고자 한다면, 본 세션을 확인해주시기 바랍니다.

iOS Memory Deep Dive 세션 요약 - Discover how memory graphs can be used to get a close up look at what is contributing to an app’s memory footprint. Understand the true memory cost of an image. Learn some tips and tricks for reducing the memory footprint of an app.

Memory Footprint

  • 메모리 사용의 최소 단위는 Page이다. 그리고 이 Page는 16KB이다.
  • Page는 Clean Page, Dirty Page가 있고 이는 Page가 write 되었는지 아닌지에 따라서 구분된다.
  • 실제 메모리 사용량 = 사용된 Page 수 * Page의 크기와 같다.

  • 위처럼 배열을 allocate하게 되면 시스템은 6개의 Page를 배열에 할당한다. 이 Page는 모두 Clean Page이다.
  • 그리고 이 배열에 새로운 값을 할당(write)하게 되면 해당 값이 포함된 Page는 Dirty Page가 된다.

  • Memory Mapped Files - 디스크에 있는 파일이지만, 메모리 위에 올라가 있는 파일

  • 다음과 같이 50KB 크기의 jpeg 이미지가 있다고 하였을 때, 이 파일은 다음과 같이 메모리에 적재된다.

Memory Profile

  • 메모리는 다음과 같이 (read, write 관점에서) Dirty, Compressed, Clean 영역으로 구분할 수 있다.

Clean Memory

  • Clean Memory는 기록될(page out, writable) 수 있는 메모리를 의미한다. Clean Memory의 데이터는 아직 write되지 않았기 때문에 디스크의 데이터와 동일하다.
  • Memory Mapped File, 프레임워크의 일부 등이 이에 해당한다.

Dirty Memory

  • Dirty Memory는 App에 의해 쓰여진(written) 메모리를 의미한다. 사용자에 의해 기록되었기 때문에 디스크와 메모리가 동일한 데이터를 가지고 있지 않다.
  • heap의 메모리 할당, decode된 이미지 버퍼, 프레임워크의 일부 등이 이에 해당한다.

Compressed Memory

  • iOS에는 전통적인 Disk Swap System이 없다.
  • 그 대신 Memory Compressor라는 것을 iOS 7부터 사용한다.

Memory Compressor는

  1. 접근이 되지 않은 page를 squeeze down(압축)하여, 더 많은 공간을 생성한다.
  2. 접근이 일어날 경우 압축을 해제하여 메모리 writing이 일어날 수 있도록 한다.

Memory Warning

  • 시스템은 사용 가능한 메모리가 부족할 때, 앱의 메모리를 정리하고, 이 때, 앱 내로 Notification(didReceiveMemoryWarning)을 전달한다.
  • 앱은 이 때 적절히 메모리 사용량을 줄이는 작업을 수행할 필요가 있다.(i.e. 메모리에 캐싱된 데이터 제거)

  • 캐시의 경우 NSCache를 사용하게 되면, 데이터를 thread safe(여러 쓰레드에서 동시에 접근해도 안전하다)하게 저장할 수 있고, 메모리에 저장된 데이터는 항상 purgeable(버릴 수 있는)하기 때문에 NSDictionary보다 NSCache를 사용하는 것을 권장한다.

  1. 메모리 footprint의 한계치는 디바이스마다 다르다.
  2. extension 앱은 더 제약이 많다.
  3. 이 한계를 넘게되면 EXC_RESOURCE_EXCEPTION이 에러로 나온다.

Tools for Profiling Footprint

  • Xcode에서 앱의 메모리 사용량을 빠르게 확인하려면 Debug Navigator에서 메모리 사용량을 보면 된다.

Instrument

  • 이 때, 메모리 사용량에 대해 좀 더 자세히 디버깅하기 위해서는 Instrument를 사용한다.

  • Instrument는 메모리 사용량을 추적하기 위한 여러 가지 템플릿을 제공하고, 많은 개발자들이 이미 Allocationleaks에 대해서는 알고 있고, 이를 활용하고 있다.
  • 다만, VM Tracker, Virtual Memory Trace에 대해서는 모르는 개발자도 많다.

VM Tracker

  • VM Tracker는 앞서서 설명했던, Dirty Memory Size, Swapped Size(iOS의 경우 Compressed Memory)를 제공한다.

“Resident memory” is memory which is currently loaded into RAM - memory which is actually being used.

iphone - What do “Dirty” and “Resident” mean in relation to Virtual Memory? - Stack Overflow

Virtual Memory Trace

(발표자료가 너무 안보여서 대체하였습니다.)

  • Virtual Memory Trace는 Virtual Memory System 중 앱과 관련된 상세한 내용을 제공한다.(i.e. 페이지 캐시 히트, Virtual Memory의 page zero fill 등)

Note: Virtual Memory Activity는 실제 구동시 상당히 느리고, Allocation 템플릿처럼 실시간으로 쌓이는 모습을 확인하기 어려웠습니다.(Xcode 9.3.1) 그래서 템플릿을 켠 상태로, 앱 녹화를 모두 마치고 종료 후 확인이 가능합니다.(이마저도 Instrument가 강제 종료되는 문제가 있으니 유의하세요.)

  • Xcode 10에서는 Instrument에서 나오는 EXC_RESOURCE_EXCEPTION를 캐치하여, 앱을 일시정지하는 기능을 제공한다.

Memory Graph(vmmap)

  • Xcode에서는 메모리의 allocation을 그림으로 보여주는 Memory graph 기능을 제공한다.
  • 또한 이 Memory Graph를 활용하여 Xcode의 Command Line Tool들을 사용할 수 있도록 기능을 제공하고 있다.

Command Line Tool을 활용하기 위한 과정은 다음과 같습니다.

  • Export Memory Graph

  • Terminal

vmmap App.memgraph
vmmap —summary App.memgraph
  • 구동 화면은 다음과 같습니다.(summary)

  • 여러가지 사이즈에 대한 정보를 제공하는데, 이 때 Swapped Size는 Precompressed Size를 의미한다.

More Detail

  • 좀 더 상세한 내용을 보기 위해서는 --summary 를 제거하면 된다.

  • 예시1. 메모리의 Text Section(None writable)

  • 예시2. 메모리의 Data Section

vmmap --pages app.memgraph | grep '.dylib' | awk '{ sum += $6 } END { print "Total Dirty Pages: " sum } '
  • 메모리의 Dirty Size가 얼마나 되는지 디버깅하기 위해서 다음과 같은 명령어를 활용할 수도 있다.(발표자 왈 Super Cool Command)

Leak

  • 예시 - Strong Reference Cycle을 가지고 있는 3개의 객체 구현

  • 다음의 명령어를 실행한다.

leaks app.memorygraph

  • leak이 일어나고 있는 객체에 대한 정보와 Retain Cycle에 대한 정보를 보여준다.

  • 또한 malloc stack logging이 켜져 있을 경우, root node의 backtrace도 보여준다.

Heap

  • heap allocation에서는 어떤 객체의 크기가 큰지, 혹은 어떤 동일한 객체가 반복적으로 생성되는지 보여준다.
heap App.memgraph
heap App.memgraph -sortBySize
heap App.memgraph -addresses all | <classes-pattern>

  • 예시 화면은 다음과 같습니다.
  • 기본적으로 heap은 count를 기준으로 sorting되며, 이 옵션은 변경할 수 있다.

  • 여기서는 NSConcrteData 클래스가 매우 큰 것을 확인할 수 있다.

  • 위의 클래스가 어디서부터 왔는지 파악하는 과정은 다음과 같다.
  • 먼저, 객체의 주소값을 확인한다.(--addresses 옵션을 활용한다.)

  • 다음으로 Malloc stack logging을 활용한다.
  • 프로젝트 빌드시 Malloc stack logging 옵션을 켤 경우, 시스템은 각각의 allocation에 대한 모든 backtrace를 기록한다.

  • malloc_history 옵션을 활용하면 해당 주소의 backtrace를 확인할 수 있다.

  • 이를 실행하면 다음과 같은 backtrace를 확인할 수 있다.

Tool 선택

  • 어떤 툴을 선택하여 메모리 사용을 디버깅할 것인지에 대해 다음 가이드라인을 참고할 수 있다.

Image

  • 이미지의 메모리 사용량은 이미지 파일 크기와 관련되어 있는 것이 아니라, 이미지의 크기와 관련되어 있다.

  • 다음과 같은 이미지가 있을 때 이 이미지의 메모리 사용량은 어떻게 될까요?

  • 정답은 대략 10MB입니다.

  • 왜 이렇게 많은 메모리를 사용하는지는 iOS에서 이미지를 어떻게 다루는지 살펴보면 알 수 있다.
  • 이미지를 메모리에 올리기 위해서는 Load, Decode, Render Phase를 거친다.

  • Load Phase에서 iOS는 압축된 이미지 사이즈를 메모리에 올린다.

  • 다음으로 Decode Phase에서 GPU가 이미지를 읽을 수 있도록 JPEG 파일의 포멧을 변경한다. 이 과정은 이미지의 압축을 모두 풀어야 하기 때문에, 이제 이미지의 사이즈는 10MB가 된다.

  • 이 이미지는 이제 우리가 원하는대로 render된다.

Image Rendering Formats

  • 이미지 랜더링 포멧 중에서 SRGB 포맷은 가장 대표적인 포맷 중 하나이다.
  • 이는 픽셀당 4 Byte이며, RGBA 각각 1 Byte를 차지한다.

  • iOS는 이제 Wide 포맷을 사용하여 SRGB 포맷보다 더 많은 색을 표현할 수 있다.
  • Wide 포맷은 픽셀당 8 Bytes이며, SRGB보다 좀 더 정확한 색을 표현할 수 있다.
  • 다만, Wide 포맷은 필요한 경우에만 사용하면 된다.

  • SRGB 포맷보다 더 작은 Luminance and alpha 8 포맷도 존재한다.
  • 이는 픽셀당 2 Bytes이고, grayscale 값과 alpha 값만 지원한다.

  • SRGB 포맷보다 75% 더 작은 Alpha 8 포맷도 존재한다.
  • 이미지 마스킹, monochrome 이미지 혹은 텍스트에서 유용하게 사용할 수 있다.

  • 이처럼, 우리에게는 많은 이미지 포맷이 있고, 우리는 올바른 이미지 포맷을 선택할 필요가 있다.

Picking the right format

  • 이미지 포맷을 선택하는 좋은 방법은 우리가 포맷을 선택하는 것이 아니라, 포맷이 우리를 선택하도록 하는 것이다.
  • UIGraphicsBeginImageContextWithOptions 는 모든 이미지를 픽셀당 4 Bytes(항상 SRGB)의 크기로 설정하고 이미지를 처리한다.
  • iOS 10에서 소개된 UIGraphicsImageRender를 사용하면, iOS 12부터는 최적의 이미지 포맷을 시스템이 선택해준다.

예시

  • 기존의 UIGraphicsBeginImageContextWithOptions를 사용하여 검은색 원을 그리는 예시이다.
  • 이 때, UIGraphicsBeginImageContextWithOptions를 사용하였기 때문에 drawing시 픽셀당 4 Bytes를 사용한다.

  • UIGraphicsImageRender 사용하여 동일하게 검은색 원을 그릴 수 있다.
  • 이 때는 UIGraphicsImageRender가 검은색만 사용한 것을 파악하였기 때문에 drawing시 픽셀당 1 Bytes만 사용할 수 있도록 해준다.
  • 보너스로 해당 마스킹을 다른 색으로 대체하고 싶을 때, 새로운 메모리 할당 없이 이 작업을 수행할 수 있다. 즉, 생성한 원을 검은색 이외의 파란색, 빨간색 등으로 추가적인 메모리 할당 없이 변경할 수 있다.

Down Sampling

  • 메모리 사용량을 줄이기 위해 이미지를 down sampling할 때, UIImage를 직접 down sampling하는 것은 앞서서 본 것처럼 Image Decompressing 작업을 하기 때문에 메모리 사용량이 높다.
  • UIKit 대신에 ImageIO 프레임워크를 사용하면, 데이터 Writing 작업 없이 이미지의 사이즈와 메타 데이터 정보를 읽을 수 있다.

  • 위의 코드는 UIImage를 곧바로 사용하여 이미지를 리사이징하는 코드이다.
  • 위의 코드에는 memory spike가 존재한다.

  • 다음 코드는 ImageIO를 활용하여 이미지를 리사이징하는 코드이다.
  • 이 코드는 low level API이기 때문에 일부 설정을 해주어야 한다.
  • 여기서 만들어지는 이미지는 CGImage이며, 이는 UIImage를 통해 wrapping해주면 바로 사용할 수 있다.
  • 이는 이전의 코드보다 50% 더 빠르고, memory spike도 없다.

Note: ImageIO를 통해서 생성되는 이미지는 앞서서 언급한 것처럼 UIImage가 아닌, CGImage입니다. CGImage는 scale과 orientation에 대한 정보를 가지고 있지 않기 때문에 scale과 orientation에 대한 정보를 기존의 이미지와 동일하게 맞추기 위해서는 이 정보를 별도로 저장하고 있어야 합니다.

Optimizing When In the background

  • 앱에서 현재 보이지 않는 부분의 이미지는 unload하면 메모리를 절약할 수 있다.

  • 방식 1. UIApplicationWillEnterForeground, UIApplicationDidEnterBackground Notification을 활용하여 이미지 로딩을 관리한다.

  • 방식 2. UIViewController의 생명주기를 활용하여(viewWillAppear, viewDidDisapper) 이미지 로딩을 관리한다.

Summary


참고 자료