androidkotlinmemory-leakskotlin-coroutinesleakcanary

lifecycle SavedStateHandlesVM instance memory leak in fragment


I have a fragment which gets data from an api, When i navigate to any other fragment i get this memory leak and
I am using the LeakCanary library to monitor memory leaks in my app. I received this memory leak and not sure how to track down what is causing it.

LeakCanaryLog:

 210 bytes retained by leaking objects
         D  Signature: 135947a80c7aa2aa27381bdc2deb41d0342a02cb
         D  ┬───
         D  │ GC Root: Global variable in native code
         D  │
         D  ├─ java.util.HashMap instance
         D  │    Leaking: NO (o↓ is not leaking)
         D  │    ↓ HashMap["view"]
         D  ├─ com.google.android.gms.ads.nonagon.ad.webview.o instance
         D  │    Leaking: NO (MainActivity↓ is not leaking and View attached)
         D  │    View is part of a window view hierarchy
         D  │    View.mAttachInfo is not null (view attached)
         D  │    View.mWindowAttachCount = 1
         D  │    mContext instance of xxx.xxx.xxxx.activity.MainActivity with mDestroyed = false
         D  │    ↓ View.mContext
         D  ├─ xxx.xxx.xxxx.activity.MainActivity instance
         D  │    Leaking: NO (GamesFragment↓ is not leaking and Activity#mDestroyed is false)
         D  │    mApplication instance of xxx.xxx.xxxx.BaseApplication
         D  │    mBase instance of androidx.appcompat.view.ContextThemeWrapper
         D  │    ↓ ComponentActivity.mOnConfigurationChangedListeners
         D  ├─ java.util.concurrent.CopyOnWriteArrayList instance
         D  │    Leaking: NO (GamesFragment↓ is not leaking)
         D  │    ↓ CopyOnWriteArrayList[4]
         D  ├─ androidx.fragment.app.FragmentManager$$ExternalSyntheticLambda0 instance
         D  │    Leaking: NO (GamesFragment↓ is not leaking)
         D  │    ↓ FragmentManager$$ExternalSyntheticLambda0.f$0
         D  ├─ androidx.fragment.app.FragmentManagerImpl instance
         D  │    Leaking: NO (GamesFragment↓ is not leaking)
         D  │    ↓ FragmentManager.mParent
         D  ├─ xxx.xxx.xxxx.fragment.GamesFragment instance
         D  │    Leaking: NO (Fragment#mFragmentManager is not null)
         D  │    ↓ Fragment.mSavedStateRegistryController
         D  │               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         D  ├─ androidx.savedstate.SavedStateRegistryController instance
         D  │    Leaking: UNKNOWN
         D  │    Retaining 17 B in 1 objects
         D  │    ↓ SavedStateRegistryController.savedStateRegistry
         D  │                                   ~~~~~~~~~~~~~~~~~~
         D  ├─ androidx.savedstate.SavedStateRegistry instance
         D  │    Leaking: UNKNOWN
         D  │    Retaining 494 B in 18 objects
         D  │    ↓ SavedStateRegistry.components
         D  │                         ~~~~~~~~~~
         D  ├─ androidx.arch.core.internal.SafeIterableMap instance
         D  │    Leaking: UNKNOWN
         D  │    Retaining 471 B in 17 objects
         D  │    ↓ SafeIterableMap["androidx.lifecycle.internal.SavedStateHandlesProvider"]
         D  │                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         D  ├─ androidx.lifecycle.SavedStateHandlesProvider instance
         D  │    Leaking: UNKNOWN
         D  │    Retaining 251 B in 10 objects
         D  │    ↓ SavedStateHandlesProvider.viewModel$delegate
         D  │                                ~~~~~~~~~~~~~~~~~~
         D  ├─ kotlin.SynchronizedLazyImpl instance
         D  │    Leaking: UNKNOWN
         D  │    Retaining 230 B in 9 objects
         D  │    ↓ SynchronizedLazyImpl._value
         D  │                           ~~~~~~
         D  ╰→ androidx.lifecycle.SavedStateHandlesVM instance
         D  ​     Leaking: YES (ObjectWatcher was watching this because androidx.lifecycle.SavedStateHandlesVM received
         D  ​     ViewModel#onCleared() callback)
         D  ​     Retaining 210 B in 8 objects
         D  ​     key = 292612b7-4d49-4a5a-a898-b2eedd194531
         D  ​     watchDurationMillis = 16225
         D  ​     retainedDurationMillis = 11225
         D  ====================================

How i navigate from one fragment to another:

private  fun loadFragment(fragment: Fragment){
    val transaction = supportFragmentManager.beginTransaction()
    transaction.replace(R.id.fragmentView,fragment)
    transaction.addToBackStack(null)
    transaction.commit()
}

Fragment Code:

class GamesFragment : DaggerFragment() {

    //Backend
    @Inject
    lateinit var gamesDealsViewModelFactory: GamesDealsViewModelFactory
    private var _gamesDealsViewModel: GamesDealsViewModel? = null
    private val gamesDealsViewModel: GamesDealsViewModel get() = _gamesDealsViewModel!!
    //RecyclerView
    private var gamesRecyclerArray: ArrayList<GamesItem> = ArrayList()
    private var gamesRecyclerAdapter: GamesRecyclerAdapter? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _view  = DataBindingUtil.inflate(inflater,R.layout.fragment_games,container,false)
        init()
        setUpListeners()
        setUpObservers()
        getStoresList()
        return view.root
    }

  
    private fun init(){
        _gamesDealsViewModel = ViewModelProvider(this@GamesFragment,gamesDealsViewModelFactory)[GamesDealsViewModel::class.java]
    }
    private fun setUpListeners(){
        view.resultsRecyclerViewLayout.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                ....
                }
            }
        })
    }
    private fun setUpObservers(){
        gamesDealsViewModel.requestStatus.observe(viewLifecycleOwner){
            if (it == "SUCCESS"){
                ....
            }else if(it != ""){
                ....
            }
        }
    }
  
    private fun setUpGamesRecycler(games: Games) {
        //Data Handel
        for (game in games){
            gamesRecyclerArray.add(game)
        }
        //Adapter Setup
        gamesRecyclerAdapter = GamesRecyclerAdapter(gamesRecyclerArray,requireActivity())
        //Adapter OnClick listener
        gamesRecyclerAdapter?.onItemClick = { game ->
           ....
        }

    }

    override fun onDestroyView() {
        view.gamesBannerAd.destroy()
        gamesRecyclerAdapter = null
        view.resultsRecyclerViewLayout.adapter = null
        mInterstitialAd = null     
        _view = null
        _gamesDealsViewModel = null
        super.onDestroyView()
    }
}

ViewModelCode:

class GamesDealsViewModel(private val gamesDealsRepository: GameDealsRepository): ViewModel(){
    private val coroutineExceptionHandler = CoroutineExceptionHandler{ _, t ->
        run {
            requestStatus.postValue(t.message.toString())
        }
    }
    fun startGamesRequest(){
        viewModelScope.launch(Dispatchers.IO+coroutineExceptionHandler) {
            gamesDealsRepository.getGamesDeals()
        }
    }

    val gamesDealsData :LiveData<Games> get() = gamesDealsRepository.games
    val requestStatus: MutableLiveData<String> get() = gamesDealsRepository.requestStatus
}

What i have tried:
-I have tried changing the observer lifecycle owner and it did not fix the leak


Solution

  • The leak trace indicates that GamesFragment is alive and attached to an alive activity, but it holds on to a SavedStateHandlesVM view model internal to jetpack which has received its onClear() callback and should be garbage collected.

    This looks like a leak inside Jetpack lifecycle. You should create a sample project that reproduces this and then submit it as a bug to the Android team (don't forget to provide library version and make sure it reproduces on the latest)