본문 바로가기
🐤2024 안드로이드/🍿 영화 프로젝트 개발 일지

Android Context 사용 시 Memory leak 방지를 위한 문제 해결

by hyeonha 2024. 3. 20.

목차

    들어가기 전에

    context를 사용하는 구글 credemtialManager을 뷰모델로 옮기는 것이 좋다는 코드 리뷰를 들었다. 

     

    memory leak (메모리 누수)란?

    메모리 누수는 애플리케이션이 더이상 필요하지 않은 객체에 대한 참조를 유지함으로써 해당 객체에 할당된 메모리를 회수할 수 없어 나타나는 오류를 말한다.

     

    메모리 누수가 언제 발생하는지 확인하기 위한 시도들

    질문 정리

     

    상황 

    AuthAcivity에서 ViewModel로 CredentialManager  관련 google login 코드를 이동하고싶다.

    CredentialManager 객체 구성 시 activityContext를 사용하고 있다.

    viewModel에서 CredentialManager  객체를 어떤 방식으로 구성해주어야할까?

    ➡️ 1. 뷰 모델의 requestGoogleLogin에서 인자로 받는 AuthActivity Context를 받아서 credentialManager 객체를 구성해주는 방법

        fun requestGoogleLogin(context: Context) {
                val credentialManager = CredentialManager.create(context)
                viewModelScope.launch {
                    runCatching {
                        credentialManager.getCredential(
                            request = credentialRequest,
                            context = context,
                        )
                    }
                      ````
                }
            }

     

    결과


    🚨메모리 leak 발생! 

    그러나 화면 회전과 같은 구성 변경 시 이 경우에도 메모리 누수가 발생하였다!


    ➡️ 2. GoogleAuthModule을 ViewModelComponet로 지정 후 에서 credentialManager 객체 구성 시 context를  @ActivityContext로 지정해줄 경우

     

    ViewModelComponent 주입 시 @ActivityContext를 이용할 수 없음

        @Provides
        fun provideCredentialManager(
            @ActivityContext context: Context,
        ): CredentialManager {
            return CredentialManager.create(context)
        }
            @Inject
            lateinit var credentialManager: CredentialManager
    
            fun requestGoogleLogin(context: Context) {
                viewModelScope.launch {
                    runCatching {
                        credentialManager.getCredential(
                            request = credentialRequest,
                            context = context,
                        )
                    }
                        ````
                }
            }

     

    🚨에러

    [Dagger/MissingBinding] @dagger.hilt.android.qualifiers.ActivityContext android.content.Context cannot be provided without an @Provides-annotated method.
    
    @dagger.hilt.android.qualifiers.ActivityContext android.content.Context is injected at
    [com.teamfilmo.filmo.app.FilmoApp_HiltComponents.ViewModelC] com.teamfilmo.filmo.data.remote.di.GoogleAuthModule.provideCredentialManager(context)
    androidx.credentials.CredentialManager is injected at
    [com.teamfilmo.filmo.app.FilmoApp_HiltComponents.ViewModelC] com.teamfilmo.filmo.ui.auth.AuthViewModel.credentialManager
    com.teamfilmo.filmo.ui.auth.AuthViewModel is injected at
    [com.teamfilmo.filmo.app.FilmoApp_HiltComponents.ViewModelC] com.teamfilmo.filmo.ui.auth.AuthViewModel_HiltModules.BindsModule.binds(arg0)
    @dagger.hilt.android.internal.lifecycle.HiltViewModelMap java.util.Map<java.lang.Class<?>,javax.inject.Provider<androidx.lifecycle.ViewModel>> is requested at
    [com.teamfilmo.filmo.app.FilmoApp_HiltComponents.ViewModelC] dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.ViewModelFactoriesEntryPoint.getHiltViewModelMap() [com.teamfilmo.filmo.app.FilmoApp_HiltComponents.SingletonC �� com.teamfilmo.filmo.app.FilmoApp_HiltComponents.ActivityRetainedC �� com.teamfilmo.filmo.app.FilmoApp_HiltComponents.ViewModelC]
    
    Note: @dagger.hilt.android.qualifiers.ActivityContext android.content.Context is provided in the following other components:
    [com.teamfilmo.filmo.app.FilmoApp_HiltComponents.ActivityC] dagger.hilt.android.internal.modules.ActivityModule.provideContext(��)

     

    ➡️ 3. GoogleAuthModule을 ViewModelComponent로 지정 후 에서 credentialManager 객체 구성 시 context를  @ApplicationContext로 지정해줄 경우

        @Provides
        fun provideCredentialManager(
            @ApplicationContext context: Context,
        ): CredentialManager {
            return CredentialManager.create(context)
        }
    }

     

     

            @Inject
            lateinit var credentialManager: CredentialManager
    
            fun requestGoogleLogin(context: Context) {
                viewModelScope.launch {
                    runCatching {
                        credentialManager.getCredential(
                            request = credentialRequest,
                            context = context,
                        )
                    }
                    ```
                }
            }

     

    결과

     

    🚨메모리 leak 발생! 
    그러나 화면 회전과 같은 구성 변경 시 액티비티가 onDestroy 가 일어나지만, 뷰 모델은 액티비티보다 생명주기가 길기때문에 유지된다.따라서 뷰 모델이 액티비티의 context를 받아와서 사용할 경우 잘못된 context를 사용하게 된다.

     

    ➡️ 4. GoogleAuthModule을 @SingletonComponent로 지정하고  provideCredentialManager를 @Singleton으로 만들어준 후  context를  @ApplicationContext로 지정해줄 경우

    @Provides
    @Singleton
    fun provideCredentialManager(
        @ApplicationContext context: Context,
    ): CredentialManager {
        return CredentialManager.create(context)
    }

     

    결과

    모든 경우에서 메모리 누수가 발생하였다.

    뷰 모델에서 액티비티의 컨텍스트를 참조하는 경우 액티비티 구성 변경 시 액티비티가 소멸되기 때문에 생명주기가 다르고 메모리 누수가 발생한다.

     

     

    해결

    미해결

     

     

     

     

    참고 

    https://www.charlezz.com/?p=44748

     

    LeakCanary로 메모리 누수 잡기 | 찰스의 안드로이드

    LeakCanary란? LeakCanary는 안드로이드를 위한 메모리 누수 감지 라이브러리다. 안드로이드 프레임워크 내부 이해를 통해 메모리 누수의 원인을 줄이는 기능을 제공하여, 개발자가 OOM(Out Of Memory)에러

    www.charlezz.com

    https://www.charlezz.com/?p=44580

     

    안드로이드의 Context를 이해하고, 메모리 누수를 방지하기 | 찰스의 안드로이드

    안드로이드의 Context란 무엇일까? Context란 사실 단어 그대로 맥락(Context)을 의미하며, 현재의 상태를 나타낸다. 다음 내용을 읽어보면 좀 더 Context를 이해하는데 도움 될 것 같다. Context에 대한 중

    www.charlezz.com

     

    728x90