IT/Android

[Android/Refactoring] Memory Leak - 3. View's Reference는 Memory Leak의 대상일까?

Hodie! 2023. 7. 28. 07:59
반응형

View's Reference는 Memory Leak의 대상일까?

이어지는 글

2023.07.21 - Memory Leak - 1. 안드로이드 앱에서의 메모리 누수 찾기

2023.07.25 - Memory Leak - 2. 자주 발생하는 안드로이드 메모리 누수 방지하기

배경

  • 지난번까지 메모리 누수를 자주 일으키는 요소들에 대해 방지하는 방법에 대해 알아보았다. 그러던 중, View를 참조하는 변수들이 메모리 누수를 일으키는 원인으로 Leakcanary에서 자주 식별되어, View의 요소를 참조하는 것이 메모리 누수를 일으키는 원인이 될 수 있는 지에 대해 알아보고자 한다.
    • Fragment에서 myTextView = view.findViewById(R.id.myTextViewId) 혹은 myTextView = binding.myTextViewId 처럼 initialize해준 뒤, ViewDestroy 되는 경우 해당 variable에 대해 null 처리를 해주지 않으면 Leak이 실제로 발생하는 지에 대한 여부를 알아보고자 한다.

View’s Reference에 대해 Null 처리하는 예시

AOSP에서의 코드

  • 실제로 AOSP(Android Open Source Project)에서의 ListFragment[Link] and PreferenceFragment[Link] 의 예시코드를 보면 ListView인 mList에 대해 mList = null를 Destory 하는 경우에 호출해 null 처리 해주는 것을 볼 수 있다.
  • 그렇다면 실제로 View를 참조하는 것이 메모리 누수를 일으킬 수 있을 확률이 있어 그러는 것일까?
  • 이와 같은 의문을 나만 가진게 아니였기에, StackOverflow를 통해 이러한 질문을 가진 사람을 어렵지 않게 찾아볼 수 있었고, 이를 참고했다.

Memory Leak 발생의 가정

  • Fragment AInstance field로, 해당 ViewchildView에 대한 reference가 있다고 가정한다.
  • Fragment A에서 Fragment B로의 navigation특정 FragmentTransaction을 사용한 FragmentManager를 통해 이루어지게 된다.
  • Transaction의 Type에 따라, ManagerView만 kill하고, Fragment Ainstance는 유지할 수 있다 (lifecycle 부분에서 “fragmentBackstack에서 layout으로 돌아간다”를 참조)
  • Fragment B에서 Fragment A다시 이동하는 경우, Fragment Ainstanceprevious instanceFront로 이동하지만, 새로운 View가 Created된다.
    • 이때, 문제는 만약 lateinit property에서 viewinstance로 유지하고, reference를 삭제하지 않으면, view가 완전히 destroyed되지 않아 Memory Leak이 발생할수 있다

→ 위와 같은 단계를 생각해보면, Fragment간 이동 중에 Memory Leak이 발생할 것이라고 예상할 수 있다.

가정에 대한 증명

사고실험

  • 먼저 단계별로, 메모리가 어떻게 할당및 해제되는 지에 대해 단계적으로 살펴보자
  1. Fragment AOpen할 때
    • 해당 Content/root ViewTextview가 메모리에 할당된다.
  2. Fragment BNaivgate할 때
    • Fragment AonDestroyView()호출되지만, Fragment AViewdestroyed되지 않는다.
      • 왜냐하면 TextViewFragment A에 대한 Strong reference유지하고 있고, Fragment ATextView에 대한 Strong Reference유지하고 있기 때문이다
      • (the TextView holds a strong reference to it and FragmentA holds a strong reference to TextView)
  3. Fragment B에서 Fragment ANavigate Back 하는 경우
    • ViewTextView이전 할당(previous allocation)이 cleared된다.
    • 또한, 동시에 onCreateView()가 호출될 때 새로운 할당(new allocation)을 받게 된다.
  4. Fragment A에서 Back을 클릭하는 경우
    • 새로운 할당이 clear 된다.

사고실험의 결론

  • 위의 단계를 살펴 봤을 때 2단계에서 Viewretain memory가 원래대로 확보되지 않아 Memory Leak이 발생하는 것을 볼 수 있다.
  • 반면 3단계부턴 유저가 Fragment로 돌아오자마자 메모리가 복구되는 것을 확인할 수 있다.
  • 따라서, FragmentManagerFragment다시 가져올 때 까지 Memory Leak이 지속됨을 알 수 있다.

실제 수행을 통한 증명: Statical Analysis (Heap Memory 이용)

  • 실제로 Button이 있는 ActivityFragments에 대한 containerFrameLayer가 있다고 하고 이는 다음과 같이 생성한다.
    • Button을 누르면 ContainerFragment A로 바뀐다.
    • Fragment A에는 ContainerFragment B로 replace하는 Button이 있다
    • Fragment BInstance FieldFragment에 저장하는 TextView가 있다

Event에 따른 heap dump의 모습과 변화하는 Retained Size
출처: https://stackoverflow.com/a/58248851

 

  • 위의 Report는 가정을 한 Application을 실행해 Operation을 실행했을 때 관찰되는 Heap Dump와 이외의 정보들이다.
  • 위의 리포트는 아래의 Operation을 실행하였을 때 나온 결과이다.
    1. Opened the app: Activity가 보인다.
    2. Pressed the Button in the Activity: FragmentA visible
    3. Pressed the Button in FragmentA: FragmentB visible 되고, FragmentA onDestroyView()
    4. Pressed the Button in the Activity: FragmentA의 2번째 instance가 Visible되고, FragmentB onDstroyView()
      (위에서 4단계로 수행했던 예시의 2번째 단계와 동일하다. Fragment BA 역할을 하고, Fragment A2번째 instanceB 역할을 한다는 점을 제외하곤)
    5. Pressed the Button in the 2nd instance FragmentA: FragmentB의 2번째 instance가 visible and FragmentA의 2번째 instance가 onDestroyView().
    6. Pressed Back Button: FragmentA의 2번째 instance가 visible and FragmentB의 2번째 instance가 onDetach()
    7. Pressed Back Button: FragmentB의 1번째 instance가 visible and FragmentA의 2번째 instance가 onDetach()
    8. Pressed Back Button: FragmentA의 1번째 instance가 visible and FragmentB의 1번째 instance가 onDetach()
    9. Pressed Back Button: FragmentA의 1번째 instance가 onDetach()
    10. Pressed Back Button: 앱 종료

Report 분석

  • Report를 봤을 때, 1단계에서 앱이 닫힐 때 까지 각각의 view모든 view는 앱이 닫힐 때까지 살아있다.
  • 2단계에서는 Fragment AView (e.g. FrameLayoutchild, Button 등)이 할당되어 있으며, 예상한대로 3단계에서 모두 clear 되었다.
  • 3단계에서는, Fragment BView (e.g. FrameLayout과 그 child, TextView 등)이 할당되어 있지만, 4단계에서는 삭제하지 않아 메모리 누수가 발생했다.
  • 그러나, 7단계에서 View다시 created 되고 새롭게 create된 view에 할당될 때, clear된다.
  • 반면에 5단계에서 생성된 View6단계에서 삭제돼, 메모리 누수가 발생하지 않았다. 이는, fragmentdetach되어, fragment가 cleard up하는 것을 prevent하지 않았기 때문이다.

실제 수행의 결과를 통한 결론

  • 이러한 과정을 통해 유저가 fragment로 돌아갈 때 까지, fragmentview를 저장함으로써 leak이 발생한다는 것을 observe할 수 있었다.
  • Fragment가 다시 돌아가는 경우 (e.g. onCreatedView()가 호출되는 등)에 leak은 다시 복구(recover)된다.
  • 반면에 fragmenttop에 있고 뒤로가기만 가능한 경우에는 어떠한 leak도 일어나지 않는다.

결론

  • 위와 같이 단계적인 증명과 이를 바탕으로 실제로 앱을 만들어 실행해보았을 때 View’s Reference들은 다른 Fragment이동했을 때 Leak을 발생시키지만, 해당 Fragment돌아갔을 때 다시 Recover되므로 현재까지 이에 대해 알지 못했더라도 큰 문제가 되지 않았던 것으로 판단된다.
  • 따라서, View’s Reference의 경우 아래와 같이 유의해서 사용하면 될 듯 하다.
    1. Fragment에서 Forward transaction이 없는 경우, ViewonDetach()에서 clear되므로, Strong referenceView를 저장하는 것에 아무런 문제가 없다.
    2. 만약, forward Transaction이 존재하는 경우, view의 weak Reference를 저장함으로써, onDestroyView()에서 clear되도록 할 수 있다.
      • 혹은 개인적으로 Destroy하는 경우에 해당 referencenull처리 함으로써 leak이 발생하지 않도록 할 수 있을 것이라 생각한다.

참고

Keeping a reference to a View in a Fragment causes memory leaks?

 

Keeping a reference to a View in a Fragment causes memory leaks?

Somebody told me the following, but I am a bit perplexed. Please, would you be able to confirm or dispute it? (the Fragment is not retained via setRetainInstance() At the moment it is a common

stackoverflow.com

반응형