안드로이드 메모리 누수 원인 정리1

Lee Yongin·2023년 11월 26일
1

안드로이드

목록 보기
10/23
post-custom-banner

좋은 앱을 만드려면 화면 렌더링을 이해하고, 메모리 자원을 효율적으로 사용해야 한다. 자원을 효율적으로 사용하려면 낭비되는 것을 막아야 한다! 그래서 대표적인 메모리 누수를 일으킬 수 있는 사례들을 정리해보려고 한다.

1. Inner Classes

아래 코드의 문제점은 내부 클래스가 외부 클래스에 대한 암묵적인 참조를 유지할 수 있다는 점이다.

class MyActivity : AppCompatActivity() {
     private inner class MyThread : Thread() {
         override fun run() {
             // Task
         }
     } 
}

왜 내부 클래스가 외부 클래스의 인스턴스에 대한 참조를 가지면 메모리누수가 일어날까?
그 원인은 내부 클래스는 혼자서는 객체를 만들 수 없고 외부 클래스의 객체가 있어야만 생성과 사용이 가능한 클래스라는 점에서 발생한다. 따라서 내부 클래스 객체의 수명이 유지되는한 외부 클래스의 객체도 가비지 컬렉팅될 수 없어서 메모리 누수가 일어날 수 있다. 이러한 누수가 위험한 이유는 묵시적이기 때문에 컴파일 오류가 발생하지 않아서 잡기 힘들다.
따라서 중첩 클래스를 사용하거나, 아예 다른 클래스로 분리하는 것이 좋다.(근데 위의 예재는 다른 클래스로 분리하더라도 MyThread라는 스레드가 Activity 보다 오래 작업하는 이상 메모리 누수는 피할 수 없을 것으로 보인다. 따라서 Activity가 소멸될 때 스레드도 중지시켜야 한다.)

중첩 클래스(Nested class) vs 내부 클래스(Inner class)

중첩 클래스에 inner 키워드를 붙이면 내부 클래스가 된다. 둘의 차이점은 중첩 클래스는 바깥쪽 클랫에 대한 참조가 없지만, 내부 클래스에는 있다는 것이다.

2. Handlers and Runnables

Handler가 외부 클래스의 인스턴스에 대한 참조를 가지고 있으면 메모리 누수를 발생한다.

class MyActivity : AppCompatActivity() {
     private val handler = Handler(Looper.getMainLooper())
     private val runnable = Runnable { /* Task */ }
     override fun onDestroy() {
         super.onDestroy()
         handler.removeCallbacks(runnable)
     } 
}

Handler는 inner class로 선언되기 때문에 외부클래스인 액티비티를 참조하고 있다. 만약 핸들러가 Looper나 MessageQueue에 작업을 예약한 상태로 액티비티가 종료되면,액티비티는 *GC되어야하는데 핸들러가 참조하고 있어서 GC되지 못한다. 따라서 아래와 같은 해결방법이 존재한다.

해결방법
1. onDestroy 함수에서 handler의 모든 callback을 제거한다.(핸들러 작업 취소)
2. handler를 static final로 선언해 기능을 수행한다.(내부클래스 -> 중첩클래스)

Handler가 inner class로 선언되기 때문에 메인 스레드의 Looper나 MessageQueue를 사용할 경우 외부 클래스가 가비지 컬렉팅되지 못하기 때문이다.

*GC:가비지 컬렉팅

3. Anonymous Listeners

아래 코드의 문제점은 이름없는 리스너를 사용해서 Activity 또는 View에 참조를 걸었기 때문이다.
UI 컴포넌트에 이벤트리스너를 많이 걸게되는데, 액티비티가 종료되기 전에 Listener를 null로 초기화하지 않으면 Activity가 종료 된 이후에도 메모리에 상주하게 되는 불상사가 발생할 수 있다.
아래의 코드는 버튼 View를 변수로 선언해주지 않아서 onDestroy에서 null로 초기화시켜줄 수 없기 때문에 메모리 누수가 발생할 것이다.

class MyActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         findViewById<Button>(R.id.myButton).setOnClickListener {
             // Click action
         }
     } 
}

참조는 메모리 누수의 직접적인 원인이라는 것을 알았다. 특히 안드로이드에서 액티비티를 사용할 때는 그 안에있는 멤버 변수의 릭까지 확인해야 한다. 다른 클래스에서 참조되어 메모리 해제가 되지 않으면 점점 사용량이 증가해지기 때문이다. 개발할 때도 중간중간 테스트를 하고 프로파일링을 하는 습관을 길러야한다.

참고자료

https://medium.com/@dugguRK/top-10-android-memory-leak-causes-9cdd8cbd5489
Kotlin IN ACTION
https://hodie.tistory.com/119#Handler%20Threads-1
https://m.blog.naver.com/yoonhok_524/221724647242
https://stackoverflow.com/questions/5002589/memory-leakage-in-event-listener?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa
https://stackoverflow.com/questions/7083441/android-alertdialog-causes-a-memory-leak

profile
⚡개발자할거야 응애 안드로이드 개발자 ⚡p.s.기록만이 살길이다!
post-custom-banner

0개의 댓글