티스토리 뷰

WWDC

[WWDC18] iOS Memory Deep Dive 정리

Hani_Levenshtein 2022. 2. 15. 01:52

안녕하세요 Hani입니다.

iOS Memory Deep Dive 정리할 거예욥.

 

 


메모리 공간은 페이지라는 단위로 나뉘며 

한 페이지에는 여러 객체가 담길 수도 있고, 한 객체가 여러 페이지를 차지할 수도 있어요.

 

OS는 메모리를 페이지 단위로 할당하게 됩니다.

 

 

 

한 페이지의 크기는 16KB = 16384(16*1024)B 이며

페이지의 종류는 크게 두 가지로 나눌 수 있어요.

 

Clean: 할당은 되었지만 쓰기 작업이 일어나지 않은 상태

Dirty: 할당되었고 동적 객체에 의해 사용되는 상태

 

 

뒤에서 Compressed 라는 페이지 종류도 소개할 거예요. 

그니까 총 3갠데 일단 두 개로 알아둡시당.

 

 

 

Int 타입을 2만개 저장할 수 있는 배열을 동적할당한 모습이에요.

2만 * int 타입 크기 / 16KB 을 소수점 올림해서 페이지 6개가 나온듯 합니당.

 

 

암튼 처음 메모리를 할당할 때는 모든 페이지가 파랗게 비어있는 Clean 상태로 주어지고

값을 할당하면 그 때부터 Dirty한 페이지가 되는 것이에욥.

 

가운데 4개 페이지는 아직 Clean.

 

 

 

Memory Mapped File은 디스크에 있지만 메모리에 로드된 파일이에요.

읽기 전용 파일을 사용한다면 항상 Clean 메모리 페이지가 됩니다.

운영체제의 Kernel은 이 파일이 디스크와 RAM에서 왔다갔다 할 수 있도록 관리해줍니다.

 

Memory Mapped File을 예로 보겠습니당.

 

 

 

 

50KB의 이미지가 메모리에 매핑될 경우 4 페이지(64KB)가 할당되고

그 중 마지막 페이지는 꽉 차있지 않은 상태가 됩니다.

따라서 빈 공간은 다른 용도로 사용될 수 있어욥.

 

 

 

위에서 말했듯이 메모리는 Dirty, Clean, Compressed로 나눌 수 있는데

 

 

 

Clean 메모리는 기록가능한 메모리를 말합니다.

Clean 메모리는 이미지, dataBlob, trainingModel, 프레임워크, Memory Mapped File 등이 있습니다.

 

따라서 모든 프레임워크는 DATA CONST 영역을 가지고 있습니다.

 

 

 

Dirty 메모리는 앱에 의해 데이터가 writed된 메모리에요.

Dirty 메모리에는 String, Array 등의 객체, decode된 이미지 버퍼나 프레임워크가 있을 수 있습니다.

 

 

프레임워크는 Data 영역과 Data Dirty 영역을 가지고 있습니다.

 

 

 

프레임워크는 clean 메모리와 dirty 메모리를 둘 다 사용하며 이는 프레임워크를 linking 하는 필수적인 부분이에요.

여기서 싱글톤과 전역 생성자는 Dirty 메모리를 줄이는 효과적인 방법입니다.

 

왜냐면 싱글톤은 생성된 후 메모리에 계속 남아있기 때문이고

전역 생성자는 프레임워크가 링크되거나 클래스가 적재될 때마다 실행되기 때문이에요.

 

 

 

iOS는 디스크 스왑 시스템을 가지고 있지 않습니다.

대신 메모리 컴프레서라는 시스템을 사용합니다.

 

 

 

메모리 컴프레서는 할당되었지만 아직 접근되지 않은 페이지를 압축하여 메모리 공간을 만들어냅니다. 

컴프레서는 메모리에 접근하는 시점에 압축했던 페이지를 디컴프레싱하여 메모리를 읽을 수 있도록 합니다.

 

 

 

애플리케이션이 항상 메모리가 부족해지는 원인은 아닙니다.

 

디바이스의 메모리가 크지 않고 통화 중인 상태라면 앱에 문제가 없더라도 메모리 워닝이 뜰 수 있어요.

메모리 경고가 발생하여 앱이 꺼지지 않도록 막기위해 조치를 취해야합니다.

 

 

 

메모리 경고가 뜨면 어떤 작업을 할 건지 뷰컨트롤러의 메서드를 통해 구현할 수 있습니다.

 

 

 

캐시를 구현할 때는 Dictionary보다 NSCache를 사용하는 것이 권장되는데

NSCache는 thread-safe하면서 purgeable한 데이터기 때문이에요.

 

 

 

 

앱의 Footprint는 Dirty 메모리와 Compressed 메모리만 고려합니다.

모든 앱은 Footprint 한계치를 가지고 있는데

이 한계치는 높은 편이지만 디바이스의 메모리 스펙에 따라 달라집니다.

 

Footprint를 초과한다면 EXC_RESOURCE_EXCEPTION이 발생합니다.

 

 

 

이제 Footprint를 추적하는 방법을 알아볼 거예요.


Tools for Profiling Footprint

debug navigator에 있는 Xcode Memory Gauge는 앱의 메모리 사용량을 그래프로 볼 수 있는 도구입니다.

 

우상단의 Profile in instruments를 눌러보면

 

 

Instruments를 볼 수 있습니다.

 

 

 

Allocation은 앱에 의한 힙 할당을 분석하고

Leak은 시간에 따른 프로세스의 메모리 누수를 확인합니다.

 

VM Tracker는 Dirty 메모리와 Compressed 메모리를 분석할 수 있고

Virtual Memory Trace는 앱의 가상 메모리 시스템의 성능에 대하여 자세히 확인할 수 있습니다.

 

 

 

 

Xcode - Product - Profile - Allocations으로 들어가면

 

 

 

VM Tracker를 볼 수 있고

 

 

Xcode - Product - Profile - System Trace로 들어가면

 

 

 

Virtual Memory Trace를 볼 수 있습니다.

 

 

 

하단 By Operation 탭에서는 Page 캐시 히트를 비롯한 가상 메모리 시스템의 성능을 볼 수 있습니다.

 

 

 

메모리가 부족해지면 위와 같은 exception을 볼 수 있습니다.

이럴 경우 앱이 멈추게 되는덴 메모리 디버거를 실행시켜 원인을 조사할 수 있습니다.

 

 

Xcode 하단의 요 버튼을 누르면

 

 

 

 

메모리 디버거를 통해 메모리 그래프를 볼 수 있습니다.

 

 

 

Xcode 좌하단에 이 버튼을 누르면 메모리 누수가 일어난 곳을 볼 수 있는데

 

 

..? 왤케 많지 

뭐 잘못했지

 

 

암튼 이 memgraph를 파일로 저장할 수 있어요. 

메모리 디버거를 켜둔 상태에서 Xcode - File - Export Memory Graph로 저장해두고나서

 

 

커멘드 라인 툴 명령으로 프로세스에 할당된 가상 메모리 공간을 출력하여

앱에서 메모리가 어떻게 사용되는지 알 수 있습니다.

 

명령어를 치고 들어가보면

 

 

가상 메모리 공간의 크기, 가상 메모리 중 Dirty 메모리 크기, Swapped 메모리 크기 등을 알 수 있습니다.

여기서 Swapped 메모리는 Compressed 메모리를 말하며

정확히는 압축된 후 메모리 크기가 아닌 압축되기 전 메모리 크기를 알려줍니다.

 

Resident Size는 물리 메모리에 적재된 Dirty 메모리와 Clean 메모리의 합.

 

 

요건 런타임에 어디에도 루팅되지 않은 힙 객체를 추적합니다.

만약 누수가 생기면 절대 해제되지 않는 Dirty 메모리가 되는 것이에요..

 

사이클을 형성하는 객체를 알려줍니다.

 

 

 

요건 힙에 할당된 객체들을 볼 수 있고

 

 

 

할당된 클래스명, 몇 개 할당됐는지 평균 크기가 얼마인지 등등.. 을 알 수 있습니다.

 

 

 

Xcode - Product - Scheme - Edit Scheme - Run - Logging에 있는 Malloc Stack을 Enable시키면

시스템이 객체가 할당되는 것을 기록합니다.

 

 

 

malloc_history 명령어를 통해 객체의 주소로 객체가 언제 어떤 이유로 생성되었는지 알 수 있습니다.

 

 

 

딱 걸림 ㅎ

 

 

 

여태 프로파일링 하는 방법을 알아봤는데욥.

 

상황에 따라 맞는 커멘드를 사용하여 프로파일링하면 되겠죠? ☺️

 

 

 

 

다음은 iOS에서 가장 메모리를 크게 잡아먹는 객체에 대하여 알아볼 거예요.


Images

이미지가 차지하는 메모리 크기는 이미지 파일의 크기와는 무관하며 이미지의 dimension에 달려있습니다.

 

위 이미지가 차지하는 메모리 사이즈는 590KB가 아니라

2048px * 1536px* 4bytes 입니다. 대략 10MB

 

여기서 4byte는 한 픽셀이 차지하는 바이트.

 

 

590KB의 파일 크기를 가진 이미지가 왜 메모리에서 10MB나 차지하게 될까욥? 🥺

 

이미지가 메모리에 적재되고 화면에 나타나기까지의 과정을 살펴보면

Load 단계에서는 운영체제가 압축된 이미지(JPEG)를 메모리에 적재시키고

Decode 단계에서는 GPU가 렌더링할 수 있도록 이미지 파일의 압축을 풀어줍니다.

Render 단계에서는 Decode된 이미지를 시각적으로 표현해줍니다.

 

 

 

Image Rendering Format 중 가장 널리쓰는 sRGB.

RGB가 각각 1 바이트를 차지하고 alpha도 1 바이트를 차지하여 픽셀 당 4 바이트를 차지합니다.

 

 

iOS 하드웨어는 조금 더 정확한 색을 표현하기 위해 픽셀 당 8 바이트의 이미지 렌더링 포맷을 사용하기도 합니다. 

 

 

반면 픽셀 당 2 바이트만 차지하는 이미지 렌더링 포맷도 있어요.

이 포맷은 Grayscale과 alpha 값만을 지원하기 때문에 Metal 앱 등에서 Shader를 위해 사용됩니다.

 

 

Alpha 8 포맷은 위에서 봤던 Luminance and Alpha 8 포맷과 달리 Alpha 값만 지원합니다.

따라서 단색 이미지를 표현하는데 사용되고 sRGB보다 메모리 사용량이 75% 더 작습니다. 

 

 

픽셀 당 1 바이트부터 8 바이트를 차지하는 이미지 렌더링 포맷을 살펴봤는데욥.

어떤 기준으로 어떤 이미지 렌더링 포맷을 선택할지가 중요하겠죵?

예를 들면 단색 이미지만 보여줄건데 픽셀 당 8 바이트짜리 포맷을 쓰면 낭비니까..! 

 

 

UIGraphicsBeginImageContextWithOptions는 iOS가 생길 때부터 쭉 써온 이미지 렌더링 포맷입니다.

항상 픽셀 당 4 바이트의 Adobe RGB의 포맷을 선택하는 녀석이에용.

 

이를 UIGraphicsImageRenderer로 바꿔주면 가장 최선의 포맷을 자동적으로 선택해줍니다. ☺️

 

 

UIGraphicsBeginImageContextWithOptions 쓰지말구

 

 

UIGraphicsImageRenderer로 쓰면 더 낫다 🥰

 

 

 

글구 또 하나

썸네일 만드는 상황처럼 이미지를 축소하는 다운스케일링을 해야할 때 UIImage를 사용하는 것은 좋지 않습니다.

내부 좌표 공간을 변형시켜야 하기 때문이에요. (음?)

 

대신 ImageIO 프레임워크를 사용하면

결과 이미지의 Dirty 메모리 비용만 소모하여 이미지를 다운샘플링할 수 있습니다.

 

 

이미지를 Resizing할 때 메모리 스파이크가 생기는 코드 🥺

이미지의 메모리 사용량을 줄이려고 리사이징하려했더니 오히려 메모리 사용량이 커진다? 흠..

 

대신 ImageIO 프레임워크를 이용하면..!

 

저수준 API를 사용하기 때문에 몇 가지 설정을 해줘야 하지만

CGImage 결과물이 생성되고 이를 UIImage로 래핑해주면 기존 코드보다 더 좋은 성능을 가질 수 있습니다.

 

 

 

이미지 마지막..!

어떤 이미지를 보고 있다가 홈 화면 이동 등으로 인해 앱이 백그라운드로 들어가면

더 이상 이미지가 메모리에 있을 필요가 없겠죠? 

 

그래서 메모리에 적재된 이미지를 다시 이동시키는 편이 좋습니다.

 

 

하나는 앱의 생명주기 메서드를 이용하는 거예욥.

앱이 백그라운드로 들어가면 이미지를 날리고

포어그라운드로 돌아오면 가져오고 ☺️

 

 

다른 방법은 뷰컨트롤러의 생명주기를 이용하여 이미지 로딩을 관리하는 것입니다. ☺️

 

 

 

 

 

 


References

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

댓글