Search code examples
androidmemory-leaksdagger-hilt

Bottomnavigationview.selectedlistener memory leak


Currently, I am investigating in some memory leaks within my app. One of the problems I have is that my bottomnavigationview is somehow causing a memory leak. My thought is, that the problem might rely on in the (perhaps) wrong usage of the databinding variable in my activity or fragment or the usage of my extension function. But this is rather odd because I am using a viewbinding / databinding delegate which should prevent such memory leak.

Activity

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private var _binding: ActivityMainBinding? = null
    private val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        changeBottomNavWhenUserLoggedIn(true)
    }

    private fun changeBottomNavWhenUserLoggedIn(loggedIn: Boolean) {
        if (loggedIn){
            setUpBottomNav(R.menu.bottom_nav_menu_logged_in)
        } else {
            setUpBottomNav(R.menu.bottom_nav_menu_logged_out)
        }
    }

    private fun setUpBottomNav(newMenu: Int) {
        with(binding.bottomNavigationView) {
            menu.clear()
            inflateMenu(newMenu)
            setupWithNavController(findNavController(R.id.fragment_container))
        }
    }

    fun hideBottomNav() {
        binding.bottomNavigationView.visibility = View.GONE
    }

    fun showBottomNav() {
        binding.bottomNavigationView.visibility = View.VISIBLE
    }

    fun isBottomNavVisible(): Boolean {
        return binding.bottomNavigationView.visibility == View.VISIBLE
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}

BaseFragment

abstract class BaseCalibrateRepairFragment(layout: Int) : Fragment(layout) {
    abstract val emailBinding: ViewDataBinding

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        hideBottomNav() <-- This is code called from the activity
    } 
}

CalibrateDataoverviewFragment

@AndroidEntryPoint
class CalibrateRepairDataOverviewFragment : BaseCalibrateRepairFragment(
    R.layout.fragment_calibrate_repair_data_overview,
) {
    // using com.kirich1409.viewbindingpropertydelegate:viewbindingpropertydelegate
    override val emailBinding: FragmentCalibrateRepairDataOverviewBinding by viewBinding()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        bindObjects()
    }

    private fun bindObjects() = with(emailBinding) {
        lifecycleOwner = viewLifecycleOwner
        viewModel = emailViewModel
        outsideTT = toolbarTitle
    }
}

Extension function (used in basefragment class)

fun Fragment.hideBottomNav() {
    if ((requireActivity() as MainActivity).isBottomNavVisible()) (requireActivity() as MainActivity).hideBottomNav()
}

LeakCanary Log

D/LeakCanary: ====================================
D/LeakCanary: HEAP ANALYSIS RESULT
D/LeakCanary: ====================================
D/LeakCanary: 1 APPLICATION LEAKS
D/LeakCanary: References underlined with "~~~" are likely causes.
D/LeakCanary: Learn more at https://squ.re/leaks.
D/LeakCanary: 24574 bytes retained by leaking objects
D/LeakCanary: Signature: f4ffffe80b845e518ea3bc4f5c13cdac5bc76
D/LeakCanary: ┬───
D/LeakCanary: │ GC Root: Global variable in native code
D/LeakCanary: │
D/LeakCanary: ├─ dalvik.system.PathClassLoader instance
D/LeakCanary: │    Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
D/LeakCanary: │    ↓ PathClassLoader.runtimeInternalObjects
D/LeakCanary: ├─ java.lang.Object[] array
D/LeakCanary: │    Leaking: NO (InternalLeakCanary↓ is not leaking)
D/LeakCanary: │    ↓ Object[].[3322]
D/LeakCanary: ├─ leakcanary.internal.InternalLeakCanary class
D/LeakCanary: │    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
D/LeakCanary: │    ↓ static InternalLeakCanary.resumedActivity
D/LeakCanary: ├─ com.example3000.app.framework.ui.view.MainActivity instance
D/LeakCanary: │    Leaking: NO (BottomNavigationView↓ is not leaking and Activity#mDestroyed is false)
D/LeakCanary: │    mApplication instance of com.example3000.app.App
D/LeakCanary: │    mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
D/LeakCanary: │    ↓ MainActivity.mainBinding
D/LeakCanary: ├─ com.example3000.app.databinding.ActivityMainBindingImpl instance
D/LeakCanary: │    Leaking: NO (BottomNavigationView↓ is not leaking)
D/LeakCanary: │    ↓ ActivityMainBindingImpl.bottomNavigationView
D/LeakCanary: ├─ com.google.android.material.bottomnavigation.BottomNavigationView instance
D/LeakCanary: │    Leaking: NO (View attached)
D/LeakCanary: │    View is part of a window view hierarchy
D/LeakCanary: │    View.mAttachInfo is not null (view attached)
D/LeakCanary: │    View.mID = R.id.bottomNavigationView
D/LeakCanary: │    View.mWindowAttachCount = 1
D/LeakCanary: │    mContext instance of com.example3000.app.framework.ui.view.MainActivity with mDestroyed = false
D/LeakCanary: │    ↓ BottomNavigationView.selectedListener
D/LeakCanary: │                           ~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ androidx.navigation.ui.NavigationUI$5 instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 12 bytes in 1 objects
D/LeakCanary: │    Anonymous class implementing com.google.android.material.bottomnavigation.
D/LeakCanary: │    BottomNavigationView$OnNavigationItemSelectedListener
D/LeakCanary: │    ↓ NavigationUI$5.val$navController
D/LeakCanary: │                     ~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ androidx.navigation.NavHostController instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 39055 bytes in 1222 objects
D/LeakCanary: │    mActivity instance of com.example3000.app.framework.ui.view.MainActivity with mDestroyed = false
D/LeakCanary: │    mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
D/LeakCanary: │    activity com.example3000.app.framework.ui.view.MainActivity with mDestroyed = false
D/LeakCanary: │    ↓ NavHostController.mOnDestinationChangedListeners
D/LeakCanary: │                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ java.util.concurrent.CopyOnWriteArrayList instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 26069 bytes in 907 objects
D/LeakCanary: │    ↓ CopyOnWriteArrayList.array
D/LeakCanary: │                           ~~~~~
D/LeakCanary: ├─ java.lang.Object[] array
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 26045 bytes in 905 objects
D/LeakCanary: │    ↓ Object[].[1]
D/LeakCanary: │               ~~~
D/LeakCanary: ├─ androidx.navigation.ui.ToolbarOnDestinationChangedListener instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 25997 bytes in 902 objects
D/LeakCanary: │    mContext instance of android.view.ContextThemeWrapper, wrapping activity com.example3000.app.framework.ui.view.
D/LeakCanary: │    MainActivity with mDestroyed = false
D/LeakCanary: │    ↓ ToolbarOnDestinationChangedListener.mContext
D/LeakCanary: │                                          ~~~~~~~~
D/LeakCanary: ├─ android.view.ContextThemeWrapper instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 25548 bytes in 891 objects
D/LeakCanary: │    mBase instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
D/LeakCanary: │    activity com.example3000.app.framework.ui.view.MainActivity with mDestroyed = false
D/LeakCanary: │    ContextThemeWrapper wraps an Activity with Activity.mDestroyed false
D/LeakCanary: │    ↓ ContextThemeWrapper.mBase
D/LeakCanary: │                          ~~~~~
D/LeakCanary: ├─ dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 25424 bytes in 885 objects
D/LeakCanary: │    mBase instance of com.example3000.app.framework.ui.view.MainActivity with mDestroyed = false
D/LeakCanary: │    ViewComponentManager$FragmentContextWrapper wraps an Activity with Activity.mDestroyed false
D/LeakCanary: │    ↓ ViewComponentManager$FragmentContextWrapper.fragment
D/LeakCanary: │                                                  ~~~~~~~~
D/LeakCanary: ╰→ com.example3000.app.framework.ui.view.fragments.home.calibrateAndRepair.CalibrateRepairDataOverviewFragment instance
D/LeakCanary: ​     Leaking: YES (ObjectWatcher was watching this because com.example3000.app.framework.ui.view.fragments.home.
D/LeakCanary: ​     calibrateAndRepair.CalibrateRepairDataOverviewFragment received Fragment#onDestroy() callback and
D/LeakCanary: ​     Fragment#mFragmentManager is null)
D/LeakCanary: ​     Retaining 24574 bytes in 851 objects
D/LeakCanary: ​     key = f8abb21d-5cf1-4e09-964a-07f5f5099164
D/LeakCanary: ​     watchDurationMillis = 8307
D/LeakCanary: ​     retainedDurationMillis = 3305
D/LeakCanary: ​     componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
D/LeakCanary: ​     wrapping activity com.example3000.app.framework.ui.view.MainActivity with mDestroyed = false
D/LeakCanary: ====================================

App HeapDump

enter image description here


Solution

  • I faced the same problem.

    NavigationUI.setupWithNavController(this, navController, configuration)
    
    public final class NavigationUI {
       navController.addOnDestinationChangedListener(...)
    

    This listener stay alive while navigationContoller living. Also this listener has reference to toolbar, that is why fragment with all of its components will not be cleared by GC. This looks like Memory leak. But:

     class ToolbarOnDestinationChangedListener(..){
        private final WeakReference<Toolbar> mToolbarWeakReference;
    

    So, it looks like memory leak, but it is not...)