개인적으로 비동기 처리는 Rx로 거의 하는 편이지만, 종종 Handler를 쓰는 경우가 있습니다. 그런데 저 같이 기본기가 부족한 개발자의 경우 아래와 같이 메모리 릭이 잠재된 코드를 작성할 수 있습니다.

메모리 릭 가능성이 있는 코드

class LeakActivity : AppCompatActivity() {

    private val TAG = LeakActivity::class.java.simpleName

    private val handler: Handler = object : Handler() {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            Log.d(TAG, "handleMessage")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_leak)

        handler.postDelayed({
            Log.d(TAG, "run!!!")
        }, 10_000)
        finish()
    }
}

액티비티를 시작하자마자 handler에게 runnable을 던지고는, 액티비티를 종료합니다. 무슨 일이 일어날까요?

사실 돌려보기도 전에 이미 Android Studio에서 아래와 같은 warning을 띄워줍니다.

This Handler class should be static or leaks might occur (anonymous android.os.Handler) less… (Ctrl+F1)
Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.

즉, 위 코드의 Handler는 inner class로 선언되었는데 이 handler가 메인 스레드의 Looper나 MessageQueue를 사용할 경우 Outer 클래스가 GC 대상이 되지 못하게 막으므로 메모리 릭이 발생할 수 있다는 내용입니다.

LeakActivity는 finish 수행 뒤 시스템에 의해 onDestory 까지 불리며 종료 흐름을 타게 됩니다. 그런 뒤 Unreachable 상태가 되어 GC 대상이 되어 메모리가 잘 회수되어야 합니다.

하지만 위 코드의 Handler는 메인 스레드의 루퍼/메세지 큐에 메세지를 보낸 상태입니다. 즉 Inner 클래스(Handler)가 Outer 클래스(Activity)의 참조를 갖고 있는 상태이므로, Outer 클래스는 여전히 Reachable 상태이기 때문에 GC 대상이 되지 않습니다.

Solution

Android Studio IDE에서 메모리 릭 가능성을 warning으로 알려줌과 동시에 해결법 역시 알려 주고 있습니다. 따라서 Inner class로 선언된 Handler를 Static inner class로 선언하면 다음과 같습니다.

class NonLeakActivity : AppCompatActivity() {

    private val TAG = NonLeakActivity::class.java.simpleName

    private val handler = NonLeakHandler()

    private class NonLeakHandler : Handler() {
        override fun handleMessage(msg: Message) {
            Log.d("TEST", "[MemoryLeak] handleMessage")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_leak)

        handler.postDelayed({
            Log.d(TAG, "run!!!")
        }, 10_000)
        finish()
    }
}

기존 inner 클래스로 선언된 Handler를 Static inner class로 변경한다고 했는데, static 키워드는 안 보입니다. 이유는 Java에서와 달리 Kotlin에는 static 키워드가 존재하지 않기 때문입니다. 대신 아래와 같이 선언하게 되면 Kotlin은 default로 static과 같은 효과를 갖게 됩니다.

class Outer {
    
    private class NonLeakHandler : Handler() {
        override fun handleMessage(msg: Message) {
            Log.d("TEST", "[MemoryLeak] handleMessage")
        }
    }

}

반대로 inner class로 선언하고 싶다면 inner라고 하는 키워드를 붙여줘야 합니다.

전체 코드는 아래 위치에서 받을 수 있습니다.
https://github.com/yoonhok524/Android-Sandbox/tree/master/HandlerLeak


0 Comments

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다