On my app, when I change theme, I get an instant memory leak notification from LeakCanary. The code I use to change theme:
fun execute(isDark: Boolean){
if (isDark) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
}
on fragment:
// set sharedPref for theme
sharedPref = requireContext().getSharedPreferences(Constants.THEME_PREF, MODE_PRIVATE)
val theme = sharedPref!!.getBoolean(Constants.SWITCH_STATE_KEY, true)
switchButton?.let { switch ->
switchButton?.isChecked = theme
switch.setOnCheckedChangeListener { _, isChecked ->
val editor = sharedPref?.edit()
editor?.putBoolean(Constants.SWITCH_STATE_KEY, isChecked)
editor?.apply()
viewModel.changeTheme(isChecked)
}
}
In all my fragments, I set every long-lasting object to null when onViewDestroy is called. For example:
override fun onDestroyView() {
super.onDestroyView()
viewModel.multipleCoinsListResponse.removeObservers(viewLifecycleOwner)
walletAdapter = null
navigation = null
menuItem?.setOnMenuItemClickListener(null)
menuItem = null
menuProvider?.let { provider ->
menuHost?.removeMenuProvider(provider)
menuProvider = null
}
menuHost = null
toolbar?.setOnClickListener(null)
toolbar = null
appBarLayout = null
actionBar = null
sharedPrefTheme = null
sharedPrefRefresh = null
mProgressDialog?.dismiss()
mProgressDialog = null
onBackPressedCallback?.remove()
onBackPressedCallback = null
constraintLayout?.removeAllViews()
constraintLayout = null
binding = null
}
Leak log states that there is a leak starting from RenderNodeAnimator and menuItem is totally leaking. Even though I set menuHost, menuProvider and menuItem to null when view is destroyed. On a forum, LeakCanary might find this RenderNodeAnimator leak when theme is changed. To test this, I tracked the app memory usage with Memory Profiler on Android Studio. When leak canary dependency was off and I try to change theme, I saw that there were no memory leaks when theme is switched. whenever I navigate to an another Fragment after switching theme, garbage collector was collecting just fine. Is this issue I am facing, should I ingore it or investigate further? Profiler is not detecting any major memory leaks but the second I switch theme, LeakCanary does. Am I missing something on my code? I'm open to any ideas or suggestions.
full leak log:
D/LeakCanary: ├─ android.graphics.animation.RenderNodeAnimator instance
D/LeakCanary: │ Leaking: UNKNOWN
D/LeakCanary: │ Retaining 243.9 kB in 6906 objects
D/LeakCanary: │ mListeners[0] = android.graphics.drawable.RippleAnimationSession$3
D/LeakCanary: │ ↓ RenderNodeAnimator.mTarget
D/LeakCanary: │ ~~~~~~~
D/LeakCanary: ├─ android.graphics.RenderNode instance
D/LeakCanary: │ Leaking: UNKNOWN
D/LeakCanary: │ Retaining 241.8 kB in 6837 objects
D/LeakCanary: │ ↓ RenderNode.mAnimationHost
D/LeakCanary: │ ~~~~~~~~~~~~~~
D/LeakCanary: ├─ android.view.ViewAnimationHostBridge instance
D/LeakCanary: │ Leaking: UNKNOWN
D/LeakCanary: │ Retaining 241.8 kB in 6836 objects
D/LeakCanary: │ ↓ ViewAnimationHostBridge.mView
D/LeakCanary: │ ~~~~~
D/LeakCanary: ├─ androidx.appcompat.view.menu.ActionMenuItemView instance
D/LeakCanary: │ Leaking: YES (View.mContext references a destroyed activity)
D/LeakCanary: │ Retaining 241.7 kB in 6835 objects
D/LeakCanary: │ View not part of a window view hierarchy
D/LeakCanary: │ View.mAttachInfo is null (view detached)
D/LeakCanary: │ View.mID = R.id.actionSettings
D/LeakCanary: │ View.mWindowAttachCount = 1
D/LeakCanary: │ mContext instance of android.view.ContextThemeWrapper, wrapping activity com.example.investmenttracker.
D/LeakCanary: │ presentation.MainActivity with mDestroyed = true
D/LeakCanary: │ ↓ View.mContext
D/LeakCanary: ├─ android.view.ContextThemeWrapper instance
D/LeakCanary: │ Leaking: YES (ActionMenuItemView↑ is leaking and ContextThemeWrapper wraps an Activity with Activity.mDestroyed
D/LeakCanary: │ true)
D/LeakCanary: │ Retaining 177 B in 9 objects
D/LeakCanary: │ mBase instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
D/LeakCanary: │ activity com.example.investmenttracker.presentation.MainActivity with mDestroyed = true
D/LeakCanary: │ ↓ ContextWrapper.mBase
D/LeakCanary: ├─ dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper instance
D/LeakCanary: │ Leaking: YES (ContextThemeWrapper↑ is leaking and ViewComponentManager$FragmentContextWrapper wraps an Activity
D/LeakCanary: │ with Activity.mDestroyed true)
D/LeakCanary: │ Retaining 40 B in 2 objects
D/LeakCanary: │ mBase instance of com.example.investmenttracker.presentation.MainActivity with mDestroyed = true
D/LeakCanary: │ ↓ ContextWrapper.mBase
D/LeakCanary: ╰→ com.example.investmenttracker.presentation.MainActivity instance
D/LeakCanary: Leaking: YES (ObjectWatcher was watching this because com.example.investmenttracker.presentation.MainActivity
D/LeakCanary: received Activity#onDestroy() callback and Activity#mDestroyed is true)
D/LeakCanary: Retaining 147.7 kB in 4816 objects
D/LeakCanary: key = b442ccea-1cb2-4f91-82c0-15f40b2e22e3
D/LeakCanary: watchDurationMillis = 5504
D/LeakCanary: retainedDurationMillis = 503
D/LeakCanary: mApplication instance of com.example.investmenttracker.CoinWiseApp
D/LeakCanary: mBase instance of androidx.appcompat.view.ContextThemeWrapper