Home > Mobile >  lifecycle SavedStateHandlesVM instance memory leak in fragment
lifecycle SavedStateHandlesVM instance memory leak in fragment

Time:11-09

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

CodePudding user response:

setRetainInstance(true) is used to retain instances of dynamic Fragments during an Activity recreation, such as a screen rotation or other config changes. This does not mean the Fragment will be retained forever by the system though.

When an Activity is terminated for other reasons, such as the user finishing the Activity (i.e. pressing back), the Fragment should be eligible for garbage collection.

CodePudding user response:

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)

  • Related