I used Jetpack's navigation to manage Fragment, which uses databinding in Fragment.
Did not add other code.
The memory leak is HomeFragment.databinding.root, which is a LinearLayout, and LinearLayout does not put anything.
The LeakCanary message is shown below:
I used Jetpack's navigation to manage Fragment, which uses databinding in Fragment.
Did not add other code.
The memory leak is HomeFragment.databinding.root, which is a LinearLayout, and LinearLayout does not put anything.
The LeakCanary message is shown below:
ApplicationLeak(className=android.widget.LinearLayout, leakTrace=
┬
├─ android.app.ActivityThread
│ Leaking: NO (ActivityThread↓ is not leaking and a class is never leaking)
│ GC Root: System class
│ ↓ static ActivityThread.sCurrentActivityThread
├─ android.app.ActivityThread
│ Leaking: NO (ArrayMap↓ is not leaking)
│ ↓ ActivityThread.mActivities
├─ android.util.ArrayMap
│ Leaking: NO (Object[]↓ is not leaking)
│ ↓ ArrayMap.mArray
├─ java.lang.Object[]
│ Leaking: NO (ActivityThread$ActivityClientRecord↓ is not leaking)
│ ↓ array Object[].[1]
├─ android.app.ActivityThread$ActivityClientRecord
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread$ActivityClientRecord.activity
├─ com.ukex.module.index.ui.MainActivity
│ Leaking: NO (FragmentController↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.mFragments
├─ androidx.fragment.app.FragmentController
│ Leaking: NO (FragmentActivity$HostCallbacks↓ is not leaking)
│ ↓ FragmentController.mHost
├─ androidx.fragment.app.FragmentActivity$HostCallbacks
│ Leaking: NO (FragmentManagerImpl↓ is not leaking)
│ ↓ FragmentActivity$HostCallbacks.mFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl
│ Leaking: NO (NavHostFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mPrimaryNav
├─ androidx.navigation.fragment.NavHostFragment
│ Leaking: NO (FragmentManagerImpl↓ is not leaking and Fragment#mFragmentManager is not null)
│ ↓ NavHostFragment.mChildFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl
│ Leaking: NO (HashMap↓ is not leaking)
│ ↓ FragmentManagerImpl.mActive
├─ java.util.HashMap
│ Leaking: NO (HashMap$Node[]↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[]
│ Leaking: NO (HashMap$Node↓ is not leaking)
│ ↓ array HashMap$Node[].[0]
├─ java.util.HashMap$Node
│ Leaking: NO (HomeFragment↓ is not leaking)
│ ↓ HashMap$Node.value
├─ com.ukex.module.index.ui.HomeFragment
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ HomeFragment.dataBinding
│ ~~~~~~~~~~~
├─ com.ukex.databinding.HomeFragmentBindingImpl
│ Leaking: UNKNOWN
│ ↓ HomeFragmentBindingImpl.mRoot
│ ~~~~~
╰→ android.widget.LinearLayout
Leaking: YES (ObjectWatcher was watching this)
mContext instance of com.ukex.module.index.ui.MainActivity with mDestroyed = false
View#mParent is null
View#mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
key = 965e1901-f293-454b-b8c2-80b869d64f9a
watchDurationMillis = 21809
retainedDurationMillis = 16807
, retainedHeapByteSize=6255)
class HomeFragment : BaseVMFragment<HomeViewModel>() {
private lateinit var dataBinding: HomeFragmentBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
dataBinding =
DataBindingUtil.inflate(inflater, R.layout.home_fragment, container, false)
return dataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
dataBinding.apply {
vm = mViewModel
lifecycleOwner = this#HomeFragment
}
btnLogin.setOnClickListener {
startActivity(Intent(context, LoginAct::class.java))
}
}
override fun bindObserve() {
super.bindObserve()
mViewModel?.user?.observe(this, Observer {
if (it != null)
ToastUtils.showLong(it.username)
})
}
override fun providerVMClass(): Class<HomeViewModel>? {
return HomeViewModel::class.java
}
}
A view should be released from memory after Fragment.onDestroyView() is called, even if the fragment is not destroyed yet.
You need to override HomeFragment.onDestroyView() and set dataBinding (or the view it wraps) to null
Related
Do you guys have any suggestion what can cause this memory leak that I got from the Leak Canary?
Below is the description I am getting from the report.
Steps to reproduce are, In the Settings menu, click on the switch which will perform the theme change, after this action, leaking happening. Below this I will provide the code piece from the SettingsFragmentClass. Any help would be most welcomed guys.
Basically issue is happening when this line is being called.
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
┬───
│ GC Root: Global variable in native code
│
├─ android.graphics.animation.RenderNodeAnimator instance
│ Leaking: UNKNOWN
│ Retaining 1.1 MB in 16344 objects
│ ↓ RenderNodeAnimator.mTarget
│ ~~~~~~~
├─ android.graphics.RenderNode instance
│ Leaking: UNKNOWN
│ Retaining 1.1 MB in 16274 objects
│ ↓ RenderNode.mHostView
│ ~~~~~~~~~
├─ com.google.android.material.switchmaterial.SwitchMaterial instance
│ Leaking: YES (View.mContext references a destroyed activity)
│ Retaining 1.1 MB in 16272 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.null
│ View.mWindowAttachCount = 1
│ mContext instance of com.flixeron.my_passkeeper.main.MainActivity with
│ mDestroyed = true
│ ↓ View.mContext
╰→ com.flixeron.my_passkeeper.main.MainActivity instance
Leaking: YES (ObjectWatcher was watching this because com.flixeron.
my_passkeeper.main.MainActivity received Activity#onDestroy() callback
and Activity#mDestroyed is true)
Retaining 251.2 kB in 5956 objects
key = 3c074833-b927-4971-b2b5-0a77b9e68bde
watchDurationMillis = 5216
retainedDurationMillis = 215
mApplication instance of com.flixeron.my_passkeeper.main.MainApplication
mBase instance of androidx.appcompat.view.ContextThemeWrapper
METADATA
Build.VERSION.SDK_INT: 31
Build.MANUFACTURER: Xiaomi
LeakCanary version: 2.7
App process name: com.flixeron.my_passkeeper
Stats: LruCache[maxSize=3000,hits=6345,misses=185335,hitRate=3%]
RandomAccess[bytes=9694284,reads=185335,travel=115234430872,range=38720254,size=
46574102]
Heap dump reason: 7 retained objects, app is visible
Analysis duration: 15049 ms
Fragment Class
class SettingsFragment : Fragment() {
private val cardViewModel: CardViewModel by viewModel()
private var bindingProp: FragmentSettingsBinding? = null
private val binding get() = bindingProp!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
bindingProp = FragmentSettingsBinding.inflate(inflater, container, false)
setTextLockTime()
configureScreenShoots()
themeSettings()
// changing dark/light mode
binding.switchTheme.setOnClickListener {
if (!PreferenceProvider.darkMode) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
PreferenceProvider.darkMode = true
binding.imageViewTheme.setImageResource(R.drawable.ic_dark_mode)
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
PreferenceProvider.darkMode = false
binding.imageViewTheme.setImageResource(R.drawable.ic_light_mode)
}
}
override fun onDestroyView() {
super.onDestroyView()
bindingProp = null
}
}
Xml file
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/switchTheme"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="30dp"
app:layout_constraintBottom_toBottomOf="#+id/textViewChangeTheme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="#+id/textViewChangeTheme">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="#+id/switchTheme1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="#+id/switchTheme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="#+id/switchTheme"
android:scaleX="1.3"
android:scaleY="1.3"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Updated leak canary
┬───
│ GC Root: System class
│
├─ android.view.inputmethod.InputMethodManager class
│ Leaking: NO (InputMethodManager↓ is not leaking and a class is never
│ leaking)
│ ↓ static InputMethodManager.sInstance
├─ android.view.inputmethod.InputMethodManager instance
│ Leaking: NO (DecorView↓ is not leaking and InputMethodManager is a
│ singleton)
│ ↓ InputMethodManager.mCurRootView
├─ android.view.ViewRootImpl instance
│ Leaking: NO (DecorView↓ is not leaking)
│ mContext instance of com.android.internal.policy.DecorContext, wrapping
│ activity com.flixeron.my_passkeeper.main.MainActivity with mDestroyed =
│ false
│ ViewRootImpl#mView is not null
│ mWindowAttributes.mTitle = "com.flixeron.my_passkeeper/com.flixeron.
│ my_passkeeper.main.MainActivity"
│ mWindowAttributes.type = 1
│ ↓ ViewRootImpl.mView
├─ com.android.internal.policy.DecorView instance
│ Leaking: NO (View attached)
│ View is part of a window view hierarchy
│ View.mAttachInfo is not null (view attached)
│ View.mWindowAttachCount = 1
│ mContext instance of com.android.internal.policy.DecorContext, wrapping
│ activity com.flixeron.my_passkeeper.main.MainActivity with mDestroyed =
│ false
│ ↓ View.mResources
│ ~~~~~~~~~~
├─ dev.b3nedikt.app_locale.AppLocaleResources instance
│ Leaking: UNKNOWN
│ Retaining 1.9 kB in 55 objects
│ context instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ AppLocaleResources.context
│ ~~~~~~~
├─ androidx.appcompat.view.ContextThemeWrapper instance
│ Leaking: UNKNOWN
│ Retaining 274.9 kB in 6390 objects
│ mBase instance of io.github.inflationx.viewpump.ViewPumpContextWrapper
│ ContextThemeWrapper does not wrap a known Android context
│ ↓ ContextWrapper.mBase
│ ~~~~~
├─ io.github.inflationx.viewpump.ViewPumpContextWrapper instance
│ Leaking: UNKNOWN
│ Retaining 270.3 kB in 6242 objects
│ mBase instance of dev.b3nedikt.app_locale.AppLocaleContextWrapper
│ ViewPumpContextWrapper does not wrap a known Android context
│ ↓ ContextWrapper.mBase
│ ~~~~~
├─ dev.b3nedikt.app_locale.AppLocaleContextWrapper instance
│ Leaking: UNKNOWN
│ Retaining 270.2 kB in 6236 objects
│ mBase instance of android.app.ContextImpl
│ AppLocaleContextWrapper does not wrap a known Android context
│ ↓ ContextWrapper.mBase
│ ~~~~~
├─ android.app.ContextImpl instance
│ Leaking: YES (ContextImpl.mOuterContext is an instance of com.flixeron.
│ my_passkeeper.main.MainActivity with Activity.mDestroyed true)
│ Retaining 268.1 kB in 6175 objects
│ mAutofillClient instance of com.flixeron.my_passkeeper.main.MainActivity
│ with mDestroyed = true
│ mOuterContext instance of com.flixeron.my_passkeeper.main.MainActivity
│ with mDestroyed = true
│ ↓ ContextImpl.mAutofillClient
╰→ com.flixeron.my_passkeeper.main.MainActivity instance
Leaking: YES (ObjectWatcher was watching this because com.flixeron.
my_passkeeper.main.MainActivity received Activity#onDestroy() callback
and Activity#mDestroyed is true)
Retaining 264.2 kB in 6091 objects
key = 44a30066-31c8-4abb-8660-907035d18457
watchDurationMillis = 5229
retainedDurationMillis = 224
mApplication instance of com.flixeron.my_passkeeper.main.MainApplication
mBase instance of androidx.appcompat.view.ContextThemeWrapper
METADATA
Build.VERSION.SDK_INT: 31
Build.MANUFACTURER: Xiaomi
LeakCanary version: 2.7
App process name: com.flixeron.my_passkeeper
Count of retained yet cleared: 1 KeyedWeakReference instances
Stats: LruCache[maxSize=3000,hits=3959,misses=160418,hitRate=2%]
RandomAccess[bytes=8403631,reads=160418,travel=86543130456,range=36387679,size=4
4147138]
Heap dump reason: 6 retained objects, app is visible
Analysis duration: 11869 ms```
your leak is strictly related to GUI, which gets restared when user change theme with your option
conider releasing GUI-related objects by releasing binded Views when Fragment gets destroyed (and further/soon will be recreated with new theme)
override fun onDestroyView() {
bindingProp = null
super.onDestroyView()
}
As per android docs I have implemented simple fragment but when I move to next fragment it started leaking in _binding even I have set binding null.
Here is my leak Stack trace
37740 bytes retained by leaking objects
Signature: 7162f0f053a181182714235f17d7e8c36154eb81
┬───
│ GC Root: Global variable in native code
│
├─ android.database.ContentObserver$Transport instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ ContentObserver$Transport.mContentObserver
├─ android.widget.Editor$HandleViewShowObserver instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ Editor$HandleViewShowObserver.mContext
├─ dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ViewComponentManager$FragmentContextWrapper wraps an Activity with Activity.mDestroyed false
│ ↓ ViewComponentManager$FragmentContextWrapper.fragment
├─ com.ics.homework.ui.auth.LoginFragment instance
│ Leaking: NO (WelcomeFragment↓ is not leaking and Fragment#mFragmentManager is not null)
│ ↓ LoginFragment.mFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mFragmentStore
├─ androidx.fragment.app.FragmentStore instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ FragmentStore.mActive
├─ java.util.HashMap instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[] array
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ HashMap$Node[].[1]
├─ java.util.HashMap$Node instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ HashMap$Node.value
├─ androidx.fragment.app.FragmentStateManager instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ FragmentStateManager.mFragment
├─ com.ics.homework.ui.auth.WelcomeFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ WelcomeFragment._binding
│ ~~~~~~~~
├─ com.ics.homework.databinding.FragmentWelcomeBindingImpl instance
│ Leaking: UNKNOWN
│ ↓ FragmentWelcomeBindingImpl.mRoot
│ ~~~~~
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
Leaking: YES (ObjectWatcher was watching this because com.ics.homework.ui.auth.WelcomeFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
key = a1bab73c-de04-4e36-bda1-b29a54da8348
watchDurationMillis = 22798
retainedDurationMillis = 17792
mContext instance of com.ics.homework.ui.auth.AuthActivity with mDestroyed = false
View#mParent is null
View#mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
====================================
0 LIBRARY LEAKS
Here is my implementation
class WelcomeFragment : Fragment() {
private var _binding: FragmentWelcomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentWelcomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.handler = onClickListener
}
private val onClickListener = View.OnClickListener { view ->
with(binding) {
when (view.id) {
btnLoginSignup.id -> {
val action = WelcomeFragmentDirections.actionWelcomeFragmentToLoginFragment()
findNavController().navigate(action)
}
tvHelpline.id,
tvHelplineNumber.id -> {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:0123456789")
startActivity(intent)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
binding.handler = null
_binding = null
}
}
In the example from documentation binding is set to null in OnDestroyView() method, not in OnDestroy(). As far as I know, OnDestroy() method of Fragment A is not called when you replace it with Fragment B and add Fragment A to backstack. That's why your binding is not null.
I got a memory leak in my login page and I can`t figure why. I am using Leak Canary to identify the leak and this was the leak trace I got.
D/LeakCanary: ┬───
│ GC Root: Input or output parameters in native code
│
├─ okio.AsyncTimeout class
│ Leaking: NO (PathClassLoader↓ is not leaking and a class is never leaking)
│ ↓ static AsyncTimeout.$class$classLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[].[957]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.eim.rdoApplication.ui.activity.MainActivity instance
│ Leaking: NO (LoginFragment↓ is not leaking and Activity#mDestroyed is false)
│ mApplication instance of com.eim.rdoApplication.AppApplication
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
│ ↓ MainActivity.mActivityResultRegistry
├─ androidx.activity.ComponentActivity$2 instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ Anonymous subclass of androidx.activity.result.ActivityResultRegistry
D/LeakCanary: │ this$0 instance of com.eim.rdoApplication.ui.activity.MainActivity with mDestroyed = false
│ ↓ ComponentActivity$2.mKeyToCallback
├─ java.util.HashMap instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[] array
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap$Node[].[2]
├─ java.util.HashMap$Node instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap$Node.value
├─ androidx.activity.result.ActivityResultRegistry$CallbackAndContract instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ ActivityResultRegistry$CallbackAndContract.mCallback
├─ androidx.fragment.app.FragmentManager$11 instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ Anonymous class implementing androidx.activity.result.ActivityResultCallback
│ ↓ FragmentManager$11.this$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mParent
├─ com.eim.rdoApplication.ui.fragment.login.LoginFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
D/LeakCanary: │ componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
│ wrapping activity com.eim.rdoApplication.ui.activity.MainActivity with mDestroyed = false
│ ↓ LoginFragment.mAnimationInfo
│ ~~~~~~~~~~~~~~
├─ androidx.fragment.app.Fragment$AnimationInfo instance
│ Leaking: UNKNOWN
│ Retaining 319257 bytes in 3340 objects
│ ↓ Fragment$AnimationInfo.mFocusedView
│ ~~~~~~~~~~~~
├─ com.google.android.material.textfield.TextInputEditText instance
│ Leaking: UNKNOWN
│ Retaining 319171 bytes in 3339 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.inputEditTextEmailField
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity com.eim.rdoApplication.ui.
│ activity.MainActivity with mDestroyed = false
│ ↓ TextInputEditText.mParent
D/LeakCanary: │ ~~~~~~~
├─ android.widget.FrameLayout instance
│ Leaking: UNKNOWN
│ Retaining 2231 bytes in 14 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity com.eim.rdoApplication.ui.
│ activity.MainActivity with mDestroyed = false
│ ↓ FrameLayout.mParent
│ ~~~~~~~
├─ com.google.android.material.textfield.TextInputLayout instance
│ Leaking: UNKNOWN
│ Retaining 204872 bytes in 2728 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.textInputLayoutEmail
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity com.eim.rdoApplication.ui.
│ activity.MainActivity with mDestroyed = false
D/LeakCanary: │ ↓ TextInputLayout.mParent
│ ~~~~~~~
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
Leaking: YES (ObjectWatcher was watching this because com.eim.rdoApplication.ui.fragment.login.LoginFragment
D/LeakCanary: received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
Retaining 4897 bytes in 71 objects
key = 0d74b072-904d-4aba-9a0e-4e17260ffc96
watchDurationMillis = 9630
retainedDurationMillis = 4627
View not part of a window view hierarchy
View.mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
activity com.eim.rdoApplication.ui.activity.MainActivity with mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 27
Build.MANUFACTURER: motorola
LeakCanary version: 2.5
App process name: com.eim.aplicativo_rdo
What could be causing this leak? Any help ?
I am working with one activity and multiple fragments being handled by navigation component.
From what I could understand, the main problem is that my textInputlayoutEmail somehow is not part of a view hierarchy at some point of the lifecycle of my fragment. This is my Fragment Code :
private const val ERROR_LOGIN = "ERROR_LOGIN"
#AndroidEntryPoint
class LoginFragment : Fragment() {
private val navController by lazy {findNavController()}
private val loginViewModel: LoginViewModel by navGraphViewModels(R.id.navigation_graph){
defaultViewModelProviderFactory
}
private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setLoadingView()
setGoToForgotPasswordFragmentButton()
setLoginButton()
super.onViewCreated(view, savedInstanceState)
}
private fun setLoginButton() {
binding.buttonSignIn.setOnClickListener {
if(areFieldsValid()){
loginViewModel.emailField = binding.inputEditTextEmailField.text.toString()
loginViewModel.passwordField = binding.editTextPasswordField.text.toString()
loginUser(loginViewModel.let { UserDataRequest(it.emailField, it.passwordField) })
}
}
}
private fun areFieldsValid(): Boolean {
return !(binding.editTextPasswordField.text.isNullOrBlank()||binding.inputEditTextEmailField.text.isNullOrBlank())
}
private fun setGoToForgotPasswordFragmentButton() {
binding.forgotPasswordEditText.setOnClickListener{
val action = LoginFragmentDirections.actionLoginFragmentToForgotPasswordFragment()
navController.navigate(action)
}
}
private fun setLoadingView() {
binding.apply {
loadingIconLoginFragment.visibility = View.INVISIBLE
loadingBgLoginFragment.visibility = View.INVISIBLE
}
}
private fun loginUser(loginUserRequest: UserDataRequest){
viewLifecycleOwner.lifecycleScope.launch {
loginViewModel.loginUser(loginUserRequest).collect {
when(it.status) {
Resource.Status.LOADING -> {
withContext(Dispatchers.Main) {
setLoadingInterface()
}
}
Resource.Status.ERROR -> {
withContext(Dispatchers.Main){
val errorMessage = "Não foi possível realizar seu Login"
clearLoadingInterface()
showErrorInterface(errorMessage, ERROR_LOGIN)
}
}
Resource.Status.SUCCESS -> {
withContext(Dispatchers.Main) {
clearLoadingInterface()
val action = LoginFragmentDirections.actionLoginFragmentToGeneralHomeFragment(0, "", "")
navController.navigate(action)
}
}
}
}
}
}
private fun setLoadingInterface() {
binding.apply {
loadingBgLoginFragment.visibility = View.VISIBLE
loadingIconLoginFragment.visibility = View.VISIBLE
}
rotateAnimation()
}
private fun showErrorInterface(errorMessage: String, errorType: String) {
clearLoadingInterface()
val dialog = NetworkErrorDialog.newInstance(errorMessage, errorType)
dialog.show(childFragmentManager, dialog.tag)
}
private fun clearLoadingInterface() {
binding.apply {
loadingBgLoginFragment.visibility = View.GONE
loadingIconLoginFragment.apply {
visibility = View.GONE
clearAnimation()
}
}
}
private fun rotateAnimation() {
val animation = AnimationUtils.loadAnimation(activity?.applicationContext, R.anim.rotate)
binding.loadingIconLoginFragment.startAnimation(animation)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
In my views so I don`t know what else can I do to clear this reference.
If anyone could show me in which part of the code I am holding a reference to this view, would help me a lot. Thanks
I have tried to set adapter null in onDestroyView also tried with addOnAttachStateChangeListener but still there is memory leak.
Here is my Stack Trace
┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static ActivityThread.sCurrentActivityThread
├─ android.app.ActivityThread instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread.mActivities
├─ android.util.ArrayMap instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ArrayMap.mArray
├─ java.lang.Object[] array
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ Object[].[1]
├─ android.app.ActivityThread$ActivityClientRecord instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread$ActivityClientRecord.activity
D/LeakCanary: ├─ com.ics.homework.ui.MainActivity instance
│ Leaking: NO (TopicFragment↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.mActivityResultRegistry
├─ androidx.activity.ComponentActivity$2 instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ Anonymous subclass of androidx.activity.result.ActivityResultRegistry
│ ↓ ComponentActivity$2.mKeyToCallback
├─ java.util.HashMap instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$HashMapEntry[] array
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ HashMap$HashMapEntry[].[1]
├─ java.util.HashMap$HashMapEntry instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ HashMap$HashMapEntry.value
├─ androidx.activity.result.ActivityResultRegistry$CallbackAndContract instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ ActivityResultRegistry$CallbackAndContract.mCallback
├─ androidx.fragment.app.FragmentManager$10 instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ Anonymous class implementing androidx.activity.result.ActivityResultCallback
│ ↓ FragmentManager$10.this$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mParent
├─ com.ics.homework.ui.course.topics.TopicFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ TopicFragment.mAnimationInfo
│ ~~~~~~~~~~~~~~
├─ androidx.fragment.app.Fragment$AnimationInfo instance
│ Leaking: UNKNOWN
│ ↓ Fragment$AnimationInfo.mFocusedView
│ ~~~~~~~~~~~~
├─ androidx.recyclerview.widget.RecyclerView instance
│ Leaking: YES (View detached and has parent)
│ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.recyclerView
│ View.mWindowAttachCount = 1
│ ↓ RecyclerView.mParent
├─ androidx.swiperefreshlayout.widget.SwipeRefreshLayout instance
│ Leaking: YES (RecyclerView↑ is leaking and View detached and has parent)
│ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.swipeRefreshLayout
│ View.mWindowAttachCount = 1
│ ↓ SwipeRefreshLayout.mParent
D/LeakCanary: ╰→ androidx.constraintlayout.widget.ConstraintLayout instance
Leaking: YES (ObjectWatcher was watching this because com.ics.homework.ui.course.topics.TopicFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
key = b5fcd20b-86ca-432c-ab69-0e4a90881651
watchDurationMillis = 26087
retainedDurationMillis = 21084
mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
View#mParent is null
View#mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
====================================
0 LIBRARY LEAKS
My Implementaion is
#AndroidEntryPoint
class TopicFragment : Fragment() {
private var courseId: String? = null
private var title: String? = null
private var _binding: FragmentTopicBinding? = null
private val binding get() = _binding!!
private val topicViewModel by viewModels<TopicViewModel>()
private lateinit var topicAdapter: TopicAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
courseId = it.getString(ARG_COURSE_ID)
title = it.getString(ARG_TITLE)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTopicBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = topicViewModel
topicAdapter = TopicAdapter(topicItemClick)
binding.apply {
stateErrorView.apply {
handler = retryCallback
}
swipeRefreshLayout.setOnRefreshListener {
topicViewModel.retry()
}
recyclerView.apply {
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
adapter = topicAdapter
setHasFixedSize(true)
}
}
observeUI()
}
private fun observeUI() {
topicViewModel.topics.observe(viewLifecycleOwner, {
Timber.e(it.status.toString())
it.data?.let(topicAdapter::submitList)
if (it.status == Status.ERROR) topicAdapter.submitList(listOf())
if (it.status != Status.LOADING) binding.swipeRefreshLayout.isRefreshing = false
})
}
private val topicItemClick = object : TopicItemClick {
override fun onClick(topic: Topic) {
val action = TopicFragmentDirections.actionTopicFragmentToChapterFragment(
topic.postId, title!!, false, topic.id, topic.topic
)
findNavController().navigate(action)
}
}
private val retryCallback = object : RetryCallback {
override fun retry() {
topicViewModel.retry()
}
}
override fun onDestroyView() {
binding.recyclerView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}
override fun onViewDetachedFromWindow(v: View) {
binding.recyclerView.adapter=null
}
})
super.onDestroyView()
_binding = null
}
companion object {
private const val ARG_COURSE_ID = "courseId"
private const val ARG_TITLE = "title"
}
}
TopicFragment is in a created state, Fragment.mAnimationInfo retains a Fragment$AnimationInfo and Fragment$AnimationInfo.mFocusedView retains the fragments detached view which should have been GCed.
This value is set by calls to Fragment.setFocusView(), and a quick search shows that's never ever cleared: https://cs.android.com/search?q=setFocusedView&sq=&ss=androidx%2Fplatform%2Fframeworks%2Fsupport
Looks like this change was introduced in 2020: https://cs.android.com/androidx/platform/frameworks/support/+/1052c3662c40176f7f02da9e06b989dcab21d500
The best would be to file an issue against the androidx fragments library.
Actually this was already filed, fixed in the next release: https://issuetracker.google.com/issues/179925887
I was working on some issues related to memory leaks. When I was removing a fragment using a fragment transaction(fragmentManager.beginTransaction().remove(fragment).commit()), I noticed memory leaks. But when I use a pop back stack (fragmentManager.popBackStack()) I didn't saw any memory leak. This scenario made me curious to find which way is better to remove a fragment. Using Fragment Transaction or using a pop back stack.
EDIT 1
I am adding a fragment dynamically.
fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int, tag: String) {
supportFragmentManager.inTransaction {
setPrimaryNavigationFragment(fragment)
addToBackStack(tag)
add(frameId, fragment, tag)
}
}
EDIT 2 Memory leak output
┬───
│ GC Root: Local variable in native code
│
├─ com.esri.arcgisruntime.mapping.view.GeoView$RenderingThread instance
│ Leaking: NO (MapView↓ is not leaking)
│ Thread name: 'Rendering thread'
│ ↓ GeoView$RenderingThread.mGeoView
├─ com.esri.arcgisruntime.mapping.view.MapView instance
│ Leaking: NO (MainActivity↓ is not leaking and View attached)
│ mContext instance of main.MainActivity with mDestroyed = false
│ View.parent android.widget.LinearLayout attached as well
│ View#mParent is set
│ View#mAttachInfo is not null (view attached)
│ View.mID = R.id.mapView
│ View.mWindowAttachCount = 1
│ ↓ MapView.mContext
├─ MainActivity instance
│ Leaking: NO (SearchContainerFragment↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.navigationHandler
├─ BottomNavigationHandler instance
│ Leaking: NO (SearchContainerFragment↓ is not leaking)
│ ↓ BottomNavigationHandler.fragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (SearchContainerFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mFragmentStore
├─ androidx.fragment.app.FragmentStore instance
│ Leaking: NO (SearchContainerFragment↓ is not leaking)
│ ↓ FragmentStore.mActive
├─ java.util.HashMap instance
│ Leaking: NO (SearchContainerFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[] array
│ Leaking: NO (SearchContainerFragment↓ is not leaking)
│ ↓ HashMap$Node[].[0]
├─ java.util.HashMap$Node instance
│ Leaking: NO (SearchContainerFragment↓ is not leaking)
│ ↓ HashMap$Node.value
├─ androidx.fragment.app.FragmentStateManager instance
│ Leaking: NO (SearchContainerFragment↓ is not leaking)
│ ↓ FragmentStateManager.mFragment
├─ SearchContainerFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ Fragment.mTag=SearchContainerFragment
│ ↓ SearchContainerFragment.adapter
│ ~~~~~~~
├─ SearchContainerAdapter instance
│ Leaking: UNKNOWN
│ ↓ SearchContainerAdapter.mViewPagerObserver
│ ~~~~~~~~~~~~~~~~~~
├─ androidx.viewpager.widget.ViewPager$PagerObserver instance
│ Leaking: UNKNOWN
│ ↓ ViewPager$PagerObserver.this$0
│ ~~~~~~
├─ NonSwipeableViewPager instance
│ Leaking: YES (View detached and has parent)
│ mContext instance of MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.viewPager
│ View.mWindowAttachCount = 1
│ ↓ NonSwipeableViewPager.mParent
╰→ android.widget.RelativeLayout instance
Leaking: YES (ObjectWatcher was watching this because SearchContainerFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
key = 6941f63f-0f30-45b3-b62e-958477398b5b
watchDurationMillis = 13855
retainedDurationMillis = 8854
key = 3b8b4ecb-d3fe-4db0-b46b-2e4ab6e64bb8
watchDurationMillis = 13856
retainedDurationMillis = 8856
mContext instance of MainActivity with mDestroyed = false
View#mParent is null
View#mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: OnePlus
LeakCanary version: 2.2
App process name: com.dev
Analysis duration: 18920 ms```