My Dialog Fragment keeping memory leak despite i already set binding = null in override of onDestroyView.
Here is my onCreateView and onDestroyView:
#AndroidEntryPoint
class AddressDialogFragment : BottomSheetDialogFragment() {
private var binding: FragmentAddressDialogBinding? = null
private val viewModel: AddressViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding =
DataBindingUtil.inflate(inflater, R.layout.fragment_address_dialog, container, false)
binding?.viewModel = viewModel
binding?.lifecycleOwner = this.viewLifecycleOwner
// doing my things
val view = binding?.root
return view!!
}
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
I use Navigation Component to navigate to this DiaLogFragment but don't think it a problem.
I have used the binding.unbind(), or dismiss() the DialogFragmet but it has no use.
I also checked that the DialogFragment already call to onDestroyView and onDestroy.
This is the leak i got from LeakCanary (2 leak on 1 time open and close the dialog, but just 1 distinct leak):
│ GC Root: System class
│
├─ android.view.accessibility.AccessibilityManager class
│ Leaking: NO (a class is never leaking)
│ ↓ static AccessibilityManager.sInstance
│ ~~~~~~~~~
├─ android.view.accessibility.AccessibilityManager instance
│ Leaking: UNKNOWN
│ Retaining 374.7 kB in 8787 objects
│ ↓ AccessibilityManager.mTouchExplorationStateChangeListeners
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
├─ android.util.ArrayMap instance
│ Leaking: UNKNOWN
│ Retaining 374.4 kB in 8775 objects
│ ↓ ArrayMap.mArray
│ ~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 374.4 kB in 8773 objects
│ ↓ Object[0]
│ ~~~
├─ com.google.android.material.textfield.DropdownMenuEndIconDelegate$7 instance
│ Leaking: UNKNOWN
│ Retaining 14.9 kB in 264 objects
│ Anonymous class implementing android.view.accessibility.AccessibilityManager$TouchExplorationStateChangeListener
│ ↓ DropdownMenuEndIconDelegate$7.this$0
│ ~~~~~~
├─ com.google.android.material.textfield.DropdownMenuEndIconDelegate instance
│ Leaking: UNKNOWN
│ Retaining 14.9 kB in 263 objects
│ context instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity com.example.aposs_buyer.
│ uicontroler.activity.AddressActivity with mDestroyed = false
│ ↓ EndIconDelegate.textInputLayout
│ ~~~~~~~~~~~~~~~
├─ com.google.android.material.textfield.TextInputLayout instance
│ Leaking: YES (View detached yet still part of window view hierarchy)
│ Retaining 12.4 kB in 298 objects
│ View is 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.example.aposs_buyer.
│ uicontroler.activity.AddressActivity with mDestroyed = false
│ ↓ View.mParent
├─ android.widget.LinearLayout instance
│ Leaking: YES (TextInputLayout↑ is leaking and View detached yet still part of window view hierarchy)
│ Retaining 157.8 kB in 3879 objects
│ View is 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.example.aposs_buyer.uicontroler.activity.AddressActivity with mDestroyed = false
│ ↓ View.mParent
╰→ androidx.core.widget.NestedScrollView instance
Leaking: YES (ObjectWatcher was watching this because com.example.aposs_buyer.uicontroler.fragment.
AddressDialogFragment received Fragment#onDestroyView() callback (references to its views should be cleared to
prevent leaks) and View detached yet still part of window view hierarchy)
Retaining 39.2 kB in 782 objects
key = 3588963a-7778-4442-ac60-a99f5380ede0
watchDurationMillis = 281964
retainedDurationMillis = 276959
View is 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.example.aposs_buyer.uicontroler.activity.AddressActivity with mDestroyed = false
I hope i will got the answer soon. I have stuck with this leak for 2 days.
I just figure out that it's the leak from TextInputLayout of the Materials Design library.
All i need todo to fix it is update Materials Design from 1.6.0 to 1.7.0
implementation 'com.google.android.material:material:1.7.0'
The relate link helps me figure it out is here: https://github.com/material-components/material-components-android/issues/2615
Here you should call dismiss() method in onPause() or in onDestroy() method. Depends on your requirement else it will create a memory leaks as the instance of bottomSheetFragment remains active.
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()
}
(The whole code can be found here (without the leakCanary dependencies): https://github.com/Dawwit0001/HiltMultiModule)
I created 2 fragments, a login fragment and a register fragment, whenever the user opens the app, the login screen is displayed
if the user navigates to the register screen, creates an account and then navigates back to the login screen a leak happens. I am not sure why is that, but I discovered that when I replace the "savedInstanceState" in the login fragment with null (inside onViewCreated), it does not happen.
The whole leak:
┬───
│ GC Root: Input or output parameters in native code
│
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never
│ leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[728]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ winged.example.hiltmultimodule.MainActivity instance
│ Leaking: NO (RegisterFragment↓ is not leaking and Activity#mDestroyed is
│ false)
│ mApplication instance of winged.example.hiltmultimodule.di.
│ HiltMultiModuleApplication
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ ComponentActivity.mOnConfigurationChangedListeners
├─ java.util.concurrent.CopyOnWriteArrayList instance
│ Leaking: NO (RegisterFragment↓ is not leaking)
│ ↓ CopyOnWriteArrayList[4]
├─ androidx.fragment.app.FragmentManager$$ExternalSyntheticLambda0 instance
│ Leaking: NO (RegisterFragment↓ is not leaking)
│ ↓ FragmentManager$$ExternalSyntheticLambda0.f$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (RegisterFragment↓ is not leaking)
│ ↓ FragmentManager.mParent
├─ winged.example.feature_login.register.RegisterFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ componentContext instance of dagger.hilt.android.internal.managers.
│ ViewComponentManager$FragmentContextWrapper, wrapping activity winged.
│ example.hiltmultimodule.MainActivity with mDestroyed = false
│ ↓ Fragment.mSavedViewState
│ ~~~~~~~~~~~~~~~
├─ android.util.SparseArray instance
│ Leaking: UNKNOWN
│ Retaining 417.7 kB in 4154 objects
│ ↓ SparseArray.mValues
│ ~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 417.6 kB in 4152 objects
│ ↓ Object[9]
│ ~~~
├─ android.widget.TextView$SavedState instance
│ Leaking: UNKNOWN
│ Retaining 416.1 kB in 4113 objects
│ ↓ TextView$SavedState.text
│ ~~~~
├─ android.text.SpannableStringBuilder instance
│ Leaking: UNKNOWN
│ Retaining 416.0 kB in 4109 objects
│ ↓ SpannableStringBuilder.mSpans
│ ~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 36 B in 1 objects
│ ↓ Object[0]
│ ~~~
├─ android.text.method.PasswordTransformationMethod$Visible instance
│ Leaking: UNKNOWN
│ Retaining 415.4 kB in 4099 objects
│ ↓ PasswordTransformationMethod$Visible.mText
│ ~~~~~
├─ androidx.emoji2.text.SpannableBuilder instance
│ Leaking: UNKNOWN
│ Retaining 415.4 kB in 4098 objects
│ ↓ SpannableStringBuilder.mSpans
│ ~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 76 B in 1 objects
│ ↓ Object[0]
│ ~~~
├─ android.widget.TextView$ChangeWatcher instance
│ Leaking: UNKNOWN
│ Retaining 16 B in 1 objects
│ ↓ TextView$ChangeWatcher.this$0
│ ~~~~~~
├─ com.google.android.material.textfield.TextInputEditText instance
│ Leaking: UNKNOWN
│ Retaining 410.0 kB in 3980 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.repeatPasswordTIET
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping
│ activity winged.example.hiltmultimodule.MainActivity with mDestroyed =
│ false
│ ↓ View.mParent
│ ~~~~~~~
├─ android.widget.FrameLayout instance
│ Leaking: UNKNOWN
│ Retaining 1.0 kB in 15 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 winged.example.hiltmultimodule.MainActivity with mDestroyed =
│ false
│ ↓ View.mParent
│ ~~~~~~~
├─ com.google.android.material.textfield.TextInputLayout instance
│ Leaking: UNKNOWN
│ Retaining 381.0 kB in 3284 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.repeatPasswordTIL
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping
│ activity winged.example.hiltmultimodule.MainActivity with mDestroyed =
│ false
│ ↓ View.mParent
│ ~~~~~~~
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
Leaking: YES (ObjectWatcher was watching this because winged.example.
feature_login.register.RegisterFragment received Fragment#onDestroyView()
callback (references to its views should be cleared to prevent leaks))
Retaining 2.5 kB in 59 objects
key = 16bf9a7e-c3de-4737-a5c2-8933c6fed9d3
watchDurationMillis = 132084
retainedDurationMillis = 127081
View not part of a window view hierarchy
View.mAttachInfo is null (view detached)
View.mID = R.id.mainCL
View.mWindowAttachCount = 1
mContext instance of dagger.hilt.android.internal.managers.
ViewComponentManager$FragmentContextWrapper, wrapping activity winged.
example.hiltmultimodule.MainActivity with mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 30
Build.MANUFACTURER: unknown
LeakCanary version: 2.10
App process name: winged.example.hiltmultimodule
Class count: 18527
Instance count: 115319
Primitive array count: 86210
Object array count: 17808
Thread count: 21
Heap total bytes: 16303680
Bitmap count: 4
Bitmap total bytes: 228214
Large bitmap count: 0
Large bitmap total bytes: 0
Db 1: open /data/user/0/winged.example.
hiltmultimodule/databases/HiltMultiModuleDB
Stats: LruCache[maxSize=3000,hits=40347,misses=84973,hitRate=32%]
RandomAccess[bytes=4231371,reads=84973,travel=25038680029,range=19100784,size=25
202710]
Analysis duration: 6049 ms
I'm still learning so any info / possible reasons / solutions will be appreciated, thanks :)
Edit:
BaseFragment:
abstract class BaseFragment<T : ViewDataBinding>(#LayoutRes private val fragmentRes: Int) : Fragment() {
private var _binding: T? = null
val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DataBindingUtil.inflate(inflater, fragmentRes, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
fun navigateTo(targetDestination: Int) {
findNavController().navigate(targetDestination)
}
fun navigateUp() {
findNavController().navigateUp()
}
}
loginFragment:
#AndroidEntryPoint
class LoginFragment : BaseFragment<FragmentLoginBinding>(R.layout.fragment_login) {
private val viewModel: LoginViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpLogInButton()
setUpTextRedirection()
observeForLoginEvents()
}
private fun setUpTextRedirection() {
binding.signUpTV.setOnClickListener {
navigateTo(R.id.registerFragment)
}
}
private fun setUpLogInButton() {
binding.logInBTN.setOnClickListener {
val email = binding.emailTIET.extractText()
val password = binding.passwordTIET.extractText()
if(email.isAValidEmail() && password.isNotBlank()) {
viewModel.logIn(LoginCredentials(mail = email, password = password))
}
}
}
private fun observeForLoginEvents() {
viewModel.loginEvent.observe(viewLifecycleOwner) { result ->
if(result.isSuccess) {
/* Adding some kind of "Main Screen" module would be an idea
but as I've stated previously, this is just a small "test" project
showing off architecture, so I hope you will forgive me <3
(PS: if you are reading this and there still isn't that module, you can make a PR
and add it)*/
Toast.makeText(requireContext(), "Success!", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(requireContext(), "No matching account", Toast.LENGTH_SHORT).show()
}
}
}
}
registerFragment:
#AndroidEntryPoint
class RegisterFragment: BaseFragment<FragmentRegisterBinding>(R.layout.fragment_register) {
private val viewModel: RegisterViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpCreateAccountButton()
setUpTextRedirection()
observeRegisterEvents()
}
private fun setUpTextRedirection() {
binding.logInTV.setOnClickListener {
navigateTo(R.id.loginFragment)
}
}
private fun setUpCreateAccountButton() {
binding.createAnAccountBTN.setOnClickListener {
val email = binding.emailTIET.extractText()
val password = binding.passwordTIET.extractText()
val repeatedPassword = binding.repeatPasswordTIET.extractText()
if(email.isAValidEmail() && (password == repeatedPassword) && password.isNotEmpty()) {
viewModel.saveUser(
LoginCredentials(mail = email, password = password)
)
}
}
}
private fun observeRegisterEvents() {
viewModel.registerEvent.observe(viewLifecycleOwner) { result ->
if(result.isSuccess) {
navigateTo(R.id.loginFragment)
} else {
Toast.makeText(requireContext(), "Something went wrong", Toast.LENGTH_SHORT).show()
}
}
}
As you may notice, the BaseFragment class has a reference to a view (binding variable), but it releases it in onDestoryView, so I think that should be working, also in the leak it isn't "complaining" about the binding itself
Found the culprit. The problem comes from an EditText that has the input type android:inputType="textPassword" or any other variant that has a password. In this case, it is one of the TextInputLayout instance, that has a TextInputEditText. But: It may need to be combined with the usage of Emoji Library, because the class has an element with the type androidx.emoji2.text.SpannableBuilder that belongs to the Emoji library.
The TextInputEditText's text is spannable, which means it's not a simple string, it's an object. An object, that can be Parcelable, which means its state can be saved. And, it looks like its actually saved here. No idea how though, since Parcelable limits which types can be saved.
The memory leak appears to be on the TextInputEditText with the ID R.id.repeatPasswordTIET. In your layout file, you can also search for #+id/repeatPasswordTIET or #id/repeatPasswordTIET to find the specific one.
Why the leak?
TextView's (or more likely EditText's) have a tendency to not remove their listeners once they are not needed. It's just not configured that way, maybe due to expecting the callers to remove the listeners themselves once they are not needed. A lot of other listeners get cleared once they are not needed, but the TextWatcher is an exception unfortunately.
Examining the leak canary trace, android.text.method.PasswordTransformationMethod$Visible instance has a androidx.emoji2.text.SpannableBuilder which contains an array, and one of the entries points to android.widget.TextView$ChangeWatcher instance which then shows the TextInputEditText that is leaked. It is leaked because in the same trace, you can see that the listener is saved to android.widget.TextView$SavedState instance, which I assume gets restored in a future fragment.
I actually tried to fetch the value myself, but wasn't able to do it. The saved state did not hold the listener.
Although, I have a potential solution: Delete every listener when the view is not necessary anymore.
Potential solution:
import android.content.Context
import android.text.TextWatcher
import android.util.AttributeSet
import com.google.android.material.textfield.TextInputEditText
class ListenerAwareEditText #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.editTextStyle
): TextInputEditText(context, attrs, defStyleAttr) {
private companion object {
val textChangedListenersStatic: MutableList<TextWatcher> = ArrayList()
}
private val textChangedListeners: MutableList<TextWatcher> = ArrayList()
/**
* Swap the listeners added in the companion object list with the actual.
*/
init {
textChangedListeners.addAll(textChangedListenersStatic)
textChangedListenersStatic.clear()
}
/**
* Overridden to hold a reference of the listener
*/
override fun addTextChangedListener(watcher: TextWatcher?) {
super.addTextChangedListener(watcher)
watcher?.let {
// NullPointerException may happen because this method
// can be called before the object itself is constructed,
// from the super classes.
// So, to hold the values, a static list in a
// companion object was used, and then the elements
// get transferred to the actual list, clearing the
// static one.
try {
textChangedListeners.add(it)
} catch (ignore: NullPointerException) {
textChangedListenersStatic.add(it)
}
}
}
/**
* Overridden to release the listener in our list
*/
override fun removeTextChangedListener(watcher: TextWatcher?) {
super.removeTextChangedListener(watcher)
watcher?.let {
// NullPointerException may happen because this method
// can be called before the object itself is constructed,
// from the super classes.
// So, to hold the values, a static list in a
// companion object was used, and then the elements
// get transferred to the actual list, clearing the
// static one.
try {
textChangedListeners.remove(it)
} catch (ignore: NullPointerException) {
textChangedListenersStatic.remove(it)
}
}
}
/**
* Clears the text changed listeners. Call this from the
* fragment's [onDestroyView] or Activity's [onDestroy].
*/
fun clearTextChangedListeners() {
textChangedListeners.forEach {
super.removeTextChangedListener(it)
}
textChangedListeners.clear()
}
}
What the class does: It caches all the listeners added in a list, and allows you to call clearTextChangedListeners() once it is not needed. (I tried to do this automatically but the lifecycle got confusing once fragments, nested recyclerviews etc... got involved so I left it here)
Usage:
Swap with your layouts' TextInputEditText with this class, and at your fragment's onDestroyView, call editText.clearTextChangedListeners().
It should solve your problem, however it's the Android world. It might not.
Edit: Apparently you should use com.google.android.material.R.attr.editTextStyle as your default theme (taken from TextInputEditText's constructor) which is updated in the code. It should not affect the design as it should be set from the XML anyway.
I'm using LeakCanary to detect leaks in the app I'm working on. And I have no idea where this one comes from. What might ExternalSyntheticLambda1 refer to? I used binding first, and I decided to remove that, since SwipeRefreshLayout leaked. It looks like removing the binding and going old school worked, however now I get a new error that I don't understand. Help? Thanks
┬───
│ GC Root: Input or output parameters in native code
│
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never
│ leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[36]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
com.app.MainActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ mApplication instance of com.app.Application
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ MainActivity.userViewModel$delegate
│ ~~~~~~~~~~~~~~~~~~~~~~
├─ kotlin.SynchronizedLazyImpl instance
│ Leaking: UNKNOWN
│ Retaining 20 B in 1 objects
│ ↓ SynchronizedLazyImpl._value
│ ~~~~~~
├─ com.app.user.UserViewModel instance
│ Leaking: UNKNOWN
│ Retaining 4.4 kB in 103 objects
│ ↓ UserViewModel._userId
│ ~~~~~~~
├─ androidx.lifecycle.MutableLiveData instance
│ Leaking: UNKNOWN
│ Retaining 427 B in 19 objects
│ ↓ LiveData.mObservers
│ ~~~~~~~~~~
├─ androidx.arch.core.internal.SafeIterableMap instance
│ Leaking: UNKNOWN
│ Retaining 368 B in 16 objects
│ ↓ SafeIterableMap[key()]
│ ~~~~~~~
├─ com.app.today.habitday.
│ HabitDayFragment$$ExternalSyntheticLambda1 instance
│ Leaking: UNKNOWN
│ Retaining 12 B in 1 objects
│ ↓ HabitDayFragment$$ExternalSyntheticLambda1.f$0
│ ~~~
╰→ com.app.today.habitday.HabitDayFragment instance
Leaking: YES (ObjectWatcher was watching this because com.app.today.habitday.HabitDayFragment received Fragment#onDestroy()
callback and Fragment#mFragmentManager is null)
Retaining 48.7 kB in 805 objects
key = 31c4bca9-d5f5-478d-b9cd-e4f826d3764c
watchDurationMillis = 5699
retainedDurationMillis = 699
METADATA
Build.VERSION.SDK_INT: 31
Build.MANUFACTURER: samsung
LeakCanary version: 2.8.1
App process name: com.app
Stats: LruCache[maxSize=3000,hits=145238,misses=327470,hitRate=30%]
RandomAccess[bytes=24498778,reads=327470,travel=129136265851,range=54226345,size
=65429917]
Analysis duration: 22458 ms
I think I'm already following all the best practices with view models and observing.
class UserViewModel(private val repository: repository,
private val chatRepository: ChatRepository,
private val crashlyticsHelper: CrashlyticsHelper,
private val systemMemoryHelper: SystemMemoryHelper) : ViewModel(), EventListener {
...
private var _userId = MutableLiveData<String>()
val userId: LiveData<String> = _userId
...
override fun onCleared() {
super.onCleared()
// Clearing listeners
chatRepository.unregisterListener(this)
}
class HabitDayFragment: Fragment(R.layout.fragment_habit_day) {
private val repository: Repository by inject()
private val analyticsHelper: AnalyticsHelper by inject()
private val userViewModel: UserViewModel by inject()
...
companion object {
fun instance(localDate: LocalDate): HabitDayFragment {
val data = Bundle()
data.putString(DATE_BUNDLE_STRING, DateUtils.localDateToString(localDate))
return HabitDayFragment().apply {
arguments = data
}
}
}
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
habitDayViewModel = ViewModelProvider(this, HabitDayViewModelFactory(repository)).get(HabitDayViewModel::class.java)
habitDayViewModel?.updateDate(localDate)
// Using lifecycleOwner here
userViewModel.userId.observe(viewLifecycleOwner, Observer {
habitDayViewModel?.setUserId(it)
})
...
// Also using lifecycle owner here, and repeat on lifecycle.
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
habitDayViewModel?.uiState?.distinctUntilChanged
....
Turns out, it was ViewPager2 that was causing the issues by not calling onDestroyView. Fixed it by going to the good ol' ViewPager v1.
More here: Fragment lifecylce behaviour using ViewPager2 and FragmentStateAdapter
Just started getting to know memory leaks with Leak Canary so forgive me if I conflate or misunderstand anything.
Leak canary tells me there is a memory leak and I've narrowed it down to this line
NavigationUI.setupActionBarWithNavController(
it,
navController
)
nav controller is called and set to the variable like this
val navController = findNavController(this#PokemonDetailFragment)
and 'it' is my activity cast to app compat activity, full snippet looks like this:
mainActivity = (activity as AppCompatActivity)
mainActivity?.let {
it.setSupportActionBar(toolbar)
val navController = findNavController(this#PokemonDetailFragment)
NavigationUI.setupActionBarWithNavController(
it,
navController
)
}
I've tried three things, 1. Injecting the activity instead, 2. Setting activity as a global nullable variable and setting it to null in onDestroyView, and 3. tried using the NavigationUI.setupWithNavController instead of the setupActionBarWithNavController which takes a toolbar and the nav controller
NavigationUI.setupWithNavController(
binding.toolbar,
findNavController(this#PokemonDetailFragment)
)
but none of these fix the issue.
Removing the first code block definitely removes the leak, however Leak Canary doesn't show the leak as being with a third party library and does say it's a variable in my code below is the heap dump
```
┬───
│ GC Root: Local variable in native code
│
├─ android.os.HandlerThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'LeakCanary-Heap-Dump'
│ ↓ HandlerThread.contextClassLoader
├─ 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[].[502]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.sealstudios.pokemonApp.MainActivity instance
│ Leaking: NO (FragmentContainerView↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity._binding
├─ com.sealstudios.pokemonApp.databinding.ActivityMainBinding instance
│ Leaking: NO (FragmentContainerView↓ is not leaking)
│ ↓ ActivityMainBinding.navHostFragment
├─ androidx.fragment.app.FragmentContainerView instance
│ Leaking: NO (View attached)
│ mContext instance of com.sealstudios.pokemonApp.MainActivity with mDestroyed = false
│ View.parent androidx.constraintlayout.widget.ConstraintLayout attached as well
│ View#mParent is set
│ View#mAttachInfo is not null (view attached)
│ View.mID = R.id.nav_host_fragment
│ View.mWindowAttachCount = 1
│ ↓ FragmentContainerView.mKeyedTags
│ ~~~~~~~~~~
├─ android.util.SparseArray instance
│ Leaking: UNKNOWN
│ ↓ SparseArray.mValues
│ ~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ ↓ Object[].[0]
│ ~~~
├─ androidx.navigation.NavHostController instance
│ Leaking: UNKNOWN
│ ↓ NavHostController.mOnDestinationChangedListeners
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
├─ java.util.concurrent.CopyOnWriteArrayList instance
│ Leaking: UNKNOWN
│ ↓ CopyOnWriteArrayList.array
│ ~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ ↓ Object[].[1]
│ ~~~
├─ androidx.navigation.ui.ActionBarOnDestinationChangedListener instance
│ Leaking: UNKNOWN
│ ↓ ActionBarOnDestinationChangedListener.mContext
│ ~~~~~~~~
├─ android.view.ContextThemeWrapper instance
│ Leaking: UNKNOWN
│ ContextThemeWrapper wraps an Activity with Activity.mDestroyed false
│ ↓ ContextThemeWrapper.mBase
│ ~~~~~
├─ dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper instance
│ Leaking: UNKNOWN
│ ViewComponentManager$FragmentContextWrapper wraps an Activity with Activity.mDestroyed false
│ ↓ ViewComponentManager$FragmentContextWrapper.fragment
│ ~~~~~~~~
╰→ com.sealstudios.pokemonApp.ui.PokemonDetailFragment instance
Leaking: YES (ObjectWatcher was watching this because com.sealstudios.pokemonApp.ui.PokemonDetailFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
key = 724affdf-d1ac-47ff-82b8-6907ced5b666
watchDurationMillis = 9052
retainedDurationMillis = 4051
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: Google
LeakCanary version: 2.4
App process name: com.sealstudios.pokemonApp
Analysis duration: 14474 ms```
any help appreciated I just want to set the toolbar which needs an AppCompatActivity and then dispose of it properly or allow the system to do it
I'm using Hilt and found this not sure if its related although my heap does mention the ContextThemeWrapper - https://github.com/google/dagger/issues/2070
Didn't get anywhere with this and setting it manually removes the memory leak
#SuppressLint("DefaultLocale")
private fun setActionBar() {
binding.toolbar.outlineProvider = null
binding.appBarLayout.outlineProvider = null
(activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
(activity as AppCompatActivity).supportActionBar.apply {
this?.setHomeButtonEnabled(true)
this?.setDisplayHomeAsUpEnabled(true)
}
}
I think the problem is with the NavigationUI.setupWithNavController() function.
Let's have a look at the code snippet below:
public static void setupWithNavController(#NonNull Toolbar toolbar,
#NonNull final NavController navController,
#NonNull final AppBarConfiguration configuration) {
navController.addOnDestinationChangedListener(
new ToolbarOnDestinationChangedListener(toolbar, configuration));
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
navigateUp(navController, configuration);
}
});
}
As we can see, you are adding a listener to your navController which is a new instance of ToolbarOnDestinationChangedListener().
Because you do not remove the listener of the navController when the activity / fragment life cycle enters the Destroy state, this is most likely causing your problem.
I strongly recommend to unset(nulling) every used object in the fragment, especially the bitmaps, in onDestroy():
#Override
public void onDestroy() {
super.onDestroy();
// Here is my example:
myBigBitmap = null;
adapter = new MyAdapter(requireContext(), new String[0], new String[0], new Bitmap[0]);
listView.setAdapter(adapter);
}
This is the only correct method. Working 100%.
I know that adding a fragment transaction to the backstack and then moving from that fragment to another fragment, the reference of the previous fragment's view is still available and it only gets destroyed when the back button is pressed.
And to avoid this, I have set the view to null in onDestroyView but the problem is, leakcanary still shows view is not null and the view reference is still available whereas logging the view says it is null.
Why is it so ?
Also, please correct me if I'm wrong or missing anything.
The fragment class-
private var mView: View? = null
private lateinit var btnSignUp: Button
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mView = inflater.inflate(R.layout.fragment_login, container, false)
return mView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnSignUp = view.findViewById(R.id.btnSignUp)
btnSignUp.setOnClickListener {
// calling function changeFragment()
changeFragment(SignUpFragment(), FragmentsTag.SIGNUP_FRAGMENT)
}
}
override fun onDestroyView() {
super.onDestroyView()
mView=null
}
LeakCanary Analysis logs --
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
43817 bytes retained by leaking objects
Signature: 6e77557c8a679dd41391c1c5badaac98217366ad
┬───
│ GC Root: System class
│
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.example.foodrunner.activities.MainActivity instance
│ Leaking: NO (LoginFragment↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.mFragments
├─ androidx.fragment.app.FragmentController instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ FragmentController.mHost
├─ androidx.fragment.app.FragmentActivity$HostCallbacks instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ FragmentActivity$HostCallbacks.mFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mActive
├─ java.util.HashMap instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$HashMapEntry[] array
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap$HashMapEntry[].[0]
├─ java.util.HashMap$HashMapEntry instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap$HashMapEntry.value
├─ com.example.foodrunner.fragments.LoginFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ Fragment.mTag=Login Fragment
│ ↓ LoginFragment.btnLogin
│ ~~~~~~~~
├─ com.google.android.material.button.MaterialButton instance
│ Leaking: YES (View detached and has parent)
│ mContext instance of com.example.foodrunner.activities.MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.btnLogin
│ View.mWindowAttachCount = 1
│ ↓ MaterialButton.mParent
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
Leaking: YES (ObjectWatcher was watching this because com.example.foodrunner.fragments.LoginFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
key = b72a82a6-b9dd-46c6-afb2-0ea6c7025001
watchDurationMillis = 9582
retainedDurationMillis = 4582
key = 0554b63a-c700-4c86-a451-b0daae06607a
watchDurationMillis = 9581
retainedDurationMillis = 4580
mContext instance of com.example.foodrunner.activities.MainActivity with mDestroyed = false
View#mParent is null
View#mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
====================================
You're still holding onto a reference to btnSignUp after onDestroyView - that is what is leaking. You have to drop all reference to all Views within the view that was just destroyed.
Therefore you should either use the same approach (make it a nullable var) or not hold onto a reference to btnSignUp in your Fragment at all - at least in your code sample, it could easily be a local variable. (In fact, the same applies to your mView - you get the View as an input to onViewCreated(), there's no reason to hold onto it at the Fragment level).
As #Rafsanjani mention you can use this too :
FragmentManager manager = getActivity().getSupportFragmentManager();
FragmentTransaction trans = manager.beginTransaction();
trans.remove(myFrag);
trans.commit();
manager.popBackStack();
Use this in your onBackPressed