안녕하세요!
오늘은 앱 UI 리펙터링을 하기 전에 화면을 A -> B -> A -> B 두 화면을 왔다 갔다를 반복하면서 마주한 메모리 점유율이 계단식으로 상승했던 이유와 어떻게 파악했는지 원인과 해결 과정을 소개하려고 합니다.
# Memgraph
# instruments - Allocations
# retain cycle
# lottie
# functional programming
화면 소개 및 메모리 계단식 점유 발생 이슈
성경 읽기 플랜 화면은 다음과 같이 구성되어 있습니다! 처음에 들어갈 때는 메모리 관련 이슈가 없지만, 3초 간격을 두며 반복해서 형광펜, 성경 읽기 플랜 두 화면을 전환하다 보면 메모리가 계단으로 점유되는 현상을 발견했습니다.
ViewController는 deinit될 수 있지만, 그럼에도 내부 컴포넌트에 의해 retain cycle이 발생할 수 있습니다!
20번의 화면 전환으로 메모리 점유율은 상승 추세를 형성한 것을 알 수 있습니다. 이는 retain cycle!을 의심할 수 있습니다.
1. 클로저에 강한참조?
2. 델리게이트 채택할때 강한 참조? 등 여러 경우가 있습니다.
따라서 이번에는
1. 메모리 점유율이 왜 이렇게 발생됬는지
2. 메모리를 주로 많이 잡아먹는 요인이 무엇인지 파악을 해보려 합니다.
1. Memgraph 사용하기
런타임에서 메모리 점유율이 432MB 최고치를 달성중이면서 점유중일 때 memgraph를 켭니다. 주요 요인이 되는 reusable view인 BibleChallengeView를 클릭해서, bytes가 높은 3개 의 ref 만 참조합니다.
어 그리고
그리고 이렇게 왼쪽으로 reference를 추려나가다 보면 4 3 2 1 이렇게 결국 4에서 참조하는 객체가 다시 1에서 자기자신이 강하게 참조 되는데 이때 진한선(string reference)입니다. (4 == 1 화살표가 끊기지 않고 무한정 반복될 수 있음)
원인은 BibleChallengeCheckCell에서 클로저(2)를 쓰는데 그 프로퍼티가 BibleChallengeView(1)을 강하게 참조했기 때문임을 알 수 있습니다.
예전에 개발해서 기억이 안난다면? 클로저 정확히 뭔지 봐보면 봐보면
옹.. checkBoxTap이라는 객체라네요.
실제로 BibleChallengeCheckCell을 보면
어 정말 그대로 있네요.
그리고 이 클로저는
여기! 이 코드는 BibleChallengeView(1) 코드 안에 117라인 입니다. handleCheckBox(_:state:)이 또한 BibleChallengeView(1)의 함수입니다. 여기서 전 functional programming을 특징인 일급객체 개념을 활용해 함수를 주입했는데, 이때 강한 참조가 형성됬다는 것을 알 수 있습니다!
막 매번 이런 강한 참조가 발생되는건 또 아닌데
이게 클로저 매개변수가 클로저를 선언한 클래스 자기자신 Self를 참조하는 class타입일 경우 외부에서 함수 type을 주입할 때 강한 참조가 생길 가능성이 높습니다. ( UIColelctionViewCell) 여기에 자기자신이 들어가는데, 이런 경우에 그렇습니다.
그럼 다시 늘 사용하던 weak 하게 클로저 주입시 self?.함수를 호출하면 됩니다.
그럼 이렇게 됩니다. 메모리는 다행히 특정한 화면에서 요구하는 점유율만큼 메모리만큼 점유하게 됩니다.
Retain cycle은 VC, VM, reactor 뿐 아니라 내부 컴포넌트에 의해서 발생될 수 도 있구.. 일급객체를 사용할때 조심해서 사용해야 합니당.
내부에서 사용하는건 좋은데 객체간 이렇게 주입할때는 조심!!
2. Instruments 활용하기
아까 언급했던 맨 처음 상승 추세를 연이어 달성하던 메모리 점유율 상황에서 Profiling을 해봤습니다. 메모리 릭 체크가 안나오네요. . . 뷰 컨이 deinit이 성공적으로 호출되서 그런가,, 그렇지만 메모리 크기가 350MB 이상을 사용하는 것을 알 수 있습니다.
여기서는 뭐가 이렇게 메모리를 많이 잡아먹었나 살펴보려 합니다.
아,,,, lottie네요. 믿고있었는데ㅠㅅㅠ
로티는 위에 영상에서 보면 달력?같이 생긴 셀 컴포넌트에서 사용되는데, BibleChallengeDayCell 클래스 안에 저렇게 정직하게 선언되어 있습니다. 이 객체를 외부, 클로저, 어디 외부로 전달하지 않습니다. 사용자에 의한 특정 이벤트가 발생되면 그때 단 한번 play()를 통해 실행됩니다.
그럼에도 lottie에 의해 메모리 점유가 지속될 수 있습니다. (저렇게 1. 로티사진 처럼 cycle을 형상하며,,) 이번에 다시 한 번 로티 자체가 메모리 점유를 많이 잡아먹는다는 것을 알게됬습니다.
그래서 이렇게 특정 이벤트 발생시 호출하고 제거하는 방식을 사용해서 메모리를 좀 더 줄였습니다.