I receive a notice from Leakcanary when debugging my app on a Samsung Galaxy S10 running on Android 12.
Indeed, Leakcanary notifies a leaked activity when toggle from light to night mode or vice versa.
Below is shown the Leakcanary report:
┬───
│ 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.my_package.activity.MainActivity
│ with mDestroyed = false
│ ViewRootImpl#mView is not null
│ mWindowAttributes.mTitle = "com.my_package.activity.MainActivity"
│ mWindowAttributes.type = 1
│ ↓ ViewRootImpl.mParentDecorView
├─ 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.my_package.activity.MainActivity
│ with mDestroyed = false
│ ↓ DecorView.mMSActions
│ ~~~~~~~~~~
├─ com.samsung.android.multiwindow.MultiSplitActions instance
│ Leaking: UNKNOWN
│ Retaining 43 B in 1 objects
│ ↓ MultiSplitActions.mWindow
│ ~~~~~~~
├─ com.android.internal.policy.PhoneWindow instance
│ Leaking: YES (Window#mDestroyed is true)
│ Retaining 15,0 kB in 286 objects
│ mContext instance of com.my_package.activity.
│ MainActivity with mDestroyed = true
│ mOnWindowDismissedCallback instance of com.my_package.activity.MainActivity with mDestroyed = true
│ ↓ Window.mContext
╰→ com.my_package.activity.MainActivity instance
Leaking: YES (ObjectWatcher was watching this because com.my_package.activity.MainActivity received
Activity#onDestroy() callback and Activity#mDestroyed is true)
Retaining 534,5 kB in 11484 objects
key = 442fa647-742b-4e2f-b4c7-452ea418ffdb
watchDurationMillis = 5868
retainedDurationMillis = 867
dataBackupAgent instance of com.my_package.activity.
MyBackupAgent
mApplication instance of com.my_package.activity.MyApplicationClass
mBase instance of androidx.appcompat.view.ContextThemeWrapper
METADATA
Build.VERSION.SDK_INT: 31
Build.MANUFACTURER: samsung
LeakCanary version: 2.8.1
Stats: LruCache[maxSize=3000,hits=129558,misses=253658,hitRate=33%]
RandomAccess[bytes=21184127,reads=253658,travel=102525154060,range=50140315,size
=69790586]
Analysis duration: 12217 ms
Please do you have any idea concerning the origin of this ?
First of all thank you for your reply.
Indeed, I have a class named MyBackupAgent:
#Singleton
class MyBackupAgent #Inject constructor(
private val backupPrefsKey: String,
private val backupHelper: MyBackupHelper
): BackupAgentHelper() {
override fun onCreate() {
super.onCreate()
addHelper(backupPrefsKey, backupHelper)
}
}
The MyBackupHelper class has the following implementation:
#Singleton
class MyBackupHelper #Inject constructor(
private val context: Context,
private val backupPreferencesKey: String
) : SharedPreferencesBackupHelper(context, backupPreferencesKey) {
.....
}
The MyBackupAgent is injected into Activity with:
#Inject
lateinit var backupAgent: MyBackupAgent
The MyBackupAgent and MyBackupHelper are provided by the MyBaseModule class:
#InstallIn(SingletonComponent::class)
#Module
class MyBaseModule {
#Singleton
#Provides
fun provideMyBackupHelper(#ApplicationContext context: Context, #BackUpPreferencesKey backupPrefsKey: String): MyBackupHelper {
val backupHelper by lazy {
MyBackupHelper(context, backupPrefsKey)
}
return backupHelper
}
#Singleton
#Provides
fun provideMyBackupAgent(#BackUpPreferencesKey backupPreferencesKey: String, backupHelper: MyBackupHelper): MyBackupAgent {
val backupAgent by lazy {
MyBackupAgent(backupPreferencesKey, backupHelper)
}
return backupAgent
}
}
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()
}
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
When initiating network communication through the Executor class, I use the showProgress function. When the result is received after communication ends, hideProgess function is used. The code is as follows and I am using it in BaseActivity.
private val networkIO = Executors.newFixedThreadPool(3)
private var progressBar: AlertDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
progressBar = AlertDialog.Builder(this)
.setView(R.layout.dialog_loading)
.setCancelable(false)
.create()
.apply { window?.setBackgroundDrawableResource(android.R.color.transparent) }
doOnCreate()
}
override fun onDestroy() {
progressBar = null
super.onDestroy()
}
fun showProgress() {
runOnUiThread {
try {
if (progressBar?.isShowing == false) {
progressBar?.show()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun hideProgress() {
runOnUiThread { progressBar?.dismiss() }
}
A memory leak occurs when moving to another activity after performing the next network communication in MainActivity.
private fun fetchCard() {
networkIO.execute {
showProgress()
networkManager.getCardInfo { _, code, message ->
Timber.d("## $code")
Timber.d("## $message")
hideProgress()
}
}
}
Leak causes provided by LeakCanary
2021-03-08 05:11:42.552 8678-8678/com.tagless D/LeakCanary:
┬───
│ 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)
│ mInitialApplication instance of com.tagless.TaglessApp
│ mSystemContext instance of android.app.ContextImpl
│ mSystemUiContext instance of android.app.ContextImpl
│ ↓ 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[].[3]
├─ android.app.ActivityThread$ActivityClientRecord instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ activity instance of com.tagless.ui.main.MainActivity with mDestroyed = false
│ ↓ ActivityThread$ActivityClientRecord.activity
├─ com.tagless.ui.main.MainActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ mApplication instance of com.tagless.TaglessApp
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ BaseActivity.progressBar
│ ~~~~~~~~~~~
├─ androidx.appcompat.app.AlertDialog instance
│ Leaking: UNKNOWN
│ Retaining 106.7 kB in 1647 objects
│ mContext instance of android.view.ContextThemeWrapper, wrapping activity com.tagless.ui.main.MainActivity with
│ mDestroyed = false
│ Dialog#mDecor is null
│ ↓ Dialog.mWindow
│ ~~~~~~~
├─ com.android.internal.policy.PhoneWindow instance
│ Leaking: UNKNOWN
│ Retaining 23.3 kB in 289 objects
│ mContext instance of android.view.ContextThemeWrapper, wrapping activity com.tagless.ui.main.MainActivity with
│ mDestroyed = false
│ Window#mDestroyed is false
│ ↓ PhoneWindow.mDecor
│ ~~~~~~
╰→ com.android.internal.policy.DecorView instance
Leaking: YES (ObjectWatcher was watching this because com.android.internal.policy.DecorView received
View#onDetachedFromWindow() callback)
Retaining 3.4 kB in 59 objects
key = 5f2f819a-9640-429f-afff-4489f7b039d2
watchDurationMillis = 5703
retainedDurationMillis = 702
View not part of a window view hierarchy
View.mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
mContext instance of android.view.ContextThemeWrapper, wrapping activity com.tagless.ui.main.MainActivity with
mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: samsung
LeakCanary version: 2.6
App process name: com.tagless
Stats: LruCache[maxSize=3000,hits=5668,misses=70891,hitRate=7%]
RandomAccess[bytes=3887192,reads=70891,travel=27244024610,range=20775300,size=25990100]
Heap dump reason: user request
Analysis duration: 3864 ms
Thanks in advance Anyone please help!!
Add progressBar = null should solve your issue. The progressBar is referenced by the activity and it can not be recycled by GC.
fun showProgress() {
runOnUiThread {
try {
if (progressBar == null) {
progressBar = AlertDialog.Builder(this)
.setView(R.layout.dialog_loading)
.setCancelable(false)
.create()
.apply { window?.setBackgroundDrawableResource(android.R.color.transparent) }
}
if (progressBar?.isShowing == false) {
progressBar?.show()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun hideProgress() {
runOnUiThread {
progressBar?.dismiss()
progressBar = 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
My problem is, that my shopViewModel which holds an instance of a paging-flow is somehow leaking. I've tried to solve this problem by converting the flow into a livedata, but that changed nothing.
ViewModel
class ShopViewModel #ViewModelInject constructor(
private val shopPagingSource: ShopPagingSource,
) : ViewModel() {
val SHOP_PAGE_CONFIG: PagingConfig = PagingConfig(pageSize = 20, enablePlaceholders = false)
// As LiveData
val shopFlow = Pager(SHOP_PAGE_CONFIG) { shopPagingSource }.flow.cachedIn(viewModelScope).asLiveData()
// Before
val shopFlow = Pager(SHOP_PAGE_CONFIG) { shopPagingSource }.flow.cachedIn(viewModelScope)
}
Fragment
#AndroidEntryPoint
class ShopFragment(private val shopListAdapter: ShopAdapter) : Fragment(R.layout.fragment_shop), ShopAdapter.OnItemClickListener {
private val shopViewModel: ShopViewModel by viewModels()
private val shopBinding: FragmentShopBinding by viewBinding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shopBinding.adapter = shopListAdapter.withLoadStateFooter(ShopLoadAdapter(shopListAdapter::retry))
shopListAdapter.clickHandler(this)
collectShopList()
}
override fun forwardClick(product: #NotNull Product) {
val action = ShopFragmentDirections.actionShopFragmentToShopItemFragment(product)
findNavController().navigate(action)
}
private fun collectShopListWithLiveData() = lifecycleScope.launch {
shopViewModel.shopFlow.observe(viewLifecycleOwner) {
lifecycleScope.launch {
shopListAdapter.submitData(it)
}
}
}
// Before converting to livedata
private fun collectShopListWithFlow() = lifecycleScope.launch {
shopViewModel.shopFlow.collectLatest {
shopListAdapter.submitData(it)
}
}
// To avoid memory leak from injected adapter
override fun onDestroyView() {
requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null
super.onDestroyView()
}
}
Adapter
class ShopAdapter #Inject constructor() : PagingDataAdapter<Product, ShopAdapter.ShopViewHolder>(Companion) {
private lateinit var clickListener: OnItemClickListener
companion object: DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(oldItem: Product, newItem: Product): Boolean = oldItem.articelNumber == newItem.articelNumber
override fun areContentsTheSame(oldItem: Product, newItem: Product): Boolean = oldItem == newItem
}
inner class ShopViewHolder(val binding: ShopListItemBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShopAdapter.ShopViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ShopListItemBinding.inflate(layoutInflater, parent, false)
return ShopViewHolder(binding).also {
binding.mcvProductItem.setOnClickListener { clickListener.forwardClick(binding.product!!) }
}
}
override fun onBindViewHolder(holder: ShopAdapter.ShopViewHolder, position: Int) {
holder.binding.product = getItem(position) ?: return
holder.binding.executePendingBindings()
}
fun clickHandler(clickEventHandler: OnItemClickListener) {
clickListener = clickEventHandler
}
interface OnItemClickListener {
fun forwardClick(product: #NotNull Product)
}
}
MainFragmentFactory
class MainFragmentFactory #Inject constructor(
// .. other dependencies
private val shopAdapter: ShopAdapter,
) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment = when(className) {
// ... other fragments
ShopFragment::class.java.name -> ShopFragment(shopAdapter)
else -> super.instantiate(classLoader, className)
}
PagingSource
class ShopPagingSource #Inject constructor(
private val shopRepository: ShopFirebaseRepository,
) : PagingSource<QuerySnapshot, Product>() {
override suspend fun load(params: LoadParams<QuerySnapshot>): LoadResult<QuerySnapshot, Product> = try {
withTimeout(SHOP_MAX_LOADING_TIME) {
val currentPage = params.key ?: shopRepository.getCurrentPage()
val lastDocumentSnapShot = currentPage.documents[currentPage.size() - 1]
val nextPage = shopRepository.getNextPage(lastDocumentSnapShot)
LoadResult.Page(
data = currentPage.toObjects(),
prevKey = null,
nextKey = nextPage
)
}
} catch (e: TimeoutCancellationException) {
Timber.d("Mediator failed, No Internet Connection")
LoadResult.Error(e)
} catch (e: ArrayIndexOutOfBoundsException) {
Timber.d("Mediator failed, ArrayIndexOutOfBounds")
LoadResult.Error(e)
} catch (e: Exception) {
Timber.d("Mediator failed, Unknown Error: ${e.message.toString()}")
LoadResult.Error(e)
}
}
LeakCanary
D/LeakCanary: ====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
2618 bytes retained by leaking objects
Signature: 944313b4ecbdb77c99682dc8c1646e12e4f37d8
┬───
│ GC Root: Local variable in native code
│
├─ 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[].[2142]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.example.app.framework.ui.view.MainActivity instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.navController$delegate
├─ kotlin.SynchronizedLazyImpl instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ ↓ SynchronizedLazyImpl._value
├─ androidx.navigation.NavHostController instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ ↓ NavHostController.mLifecycleOwner
├─ com.example.app.framework.ui.view.utils.MainNavHostFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ MainNavHostFragment.mainFragmentFactory
│ ~~~~~~~~~~~~~~~~~~~
├─ com.example.app.framework.ui.view.utils.MainFragmentFactory instance
│ Leaking: UNKNOWN
│ ↓ MainFragmentFactory.shopAdapter
│ ~~~~~~~~~~~
├─ com.example.app.framework.ui.adapter.recyclerview.ShopAdapter instance
│ Leaking: UNKNOWN
│ ↓ ShopAdapter.differ
│ ~~~~~~
├─ androidx.paging.AsyncPagingDataDiffer instance
│ Leaking: UNKNOWN
│ ↓ AsyncPagingDataDiffer.differBase
│ ~~~~~~~~~~
├─ androidx.paging.AsyncPagingDataDiffer$differBase$1 instance
│ Leaking: UNKNOWN
│ Anonymous subclass of androidx.paging.PagingDataDiffer
│ ↓ AsyncPagingDataDiffer$differBase$1.receiver
│ ~~~~~~~~
├─ androidx.paging.PageFetcher$PagerUiReceiver instance
│ Leaking: UNKNOWN
│ ↓ PageFetcher$PagerUiReceiver.this$0
│ ~~~~~~
├─ androidx.paging.PageFetcher instance
│ Leaking: UNKNOWN
│ ↓ PageFetcher.pagingSourceFactory
│ ~~~~~~~~~~~~~~~~~~~
├─ com.example.app.framework.ui.viewmodel.ShopViewModel$shopFlow$1 instance
│ Leaking: UNKNOWN
│ Anonymous subclass of kotlin.jvm.internal.Lambda
│ ↓ ShopViewModel$shopFlow$1.this$0
│ ~~~~~~
╰→ com.example.app.framework.ui.viewmodel.ShopViewModel instance
Leaking: YES (ObjectWatcher was watching this because com.example.app.framework.ui.viewmodel.ShopViewModel received ViewModel#onCleared() callback)
key = 0e65fcab-e6dd-475a-83d4-87b2050d797b
watchDurationMillis = 7771
retainedDurationMillis = 2769
====================================
EDIT
When scoping the ShopAdapter with #FragmentScoped, I get the following leak:
┬───
│ GC Root: Local variable in native code
│
├─ 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[].[409]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.example.app.framework.ui.view.MainActivity instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│ mApplication instance of com.example.app.App
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
│ ↓ MainActivity.navController$delegate
├─ kotlin.SynchronizedLazyImpl instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ ↓ SynchronizedLazyImpl._value
├─ androidx.navigation.NavHostController instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ mActivity instance of com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
│ activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│ ↓ NavHostController.mLifecycleOwner
├─ com.example.app.framework.ui.view.utils.MainNavHostFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
│ wrapping activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│ ↓ MainNavHostFragment.mainFragmentFactory
│ ~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ com.example.app.framework.ui.view.utils.MainFragmentFactory instance
│ Leaking: UNKNOWN
│ Retaining 212 bytes in 7 objects
│ ↓ MainFragmentFactory.shopAdapter
│ ~~~~~~~~~~~
├─ com.example.app.framework.ui.adapter.recyclerview.ShopAdapter instance
│ Leaking: UNKNOWN
│ Retaining 14461 bytes in 546 objects
│ ↓ ShopAdapter.clickListener
│ ~~~~~~~~~~~~~
╰→ com.example.app.framework.ui.view.fragments.shop.ShopFragment instance
Leaking: YES (ObjectWatcher was watching this because com.example.app.framework.ui.view.fragments.shop.
ShopFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
Retaining 2121 bytes in 79 objects
key = 71ec5094-8509-47a5-9e0a-070fe642ca8a
watchDurationMillis = 18366
retainedDurationMillis = 13365
componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
wrapping activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
I know that I'm late for the party but I can see that you are using lifecycleScope.launch a lot and inside you are calling the adapter to submitData. This means this adapter will not be able to be properly garbage collected. This is probably the root of memory leak.
Try to use viewLifecycleOwner.lifecycleScope.launch instead.
This is a known mistake:
https://proandroiddev.com/5-common-mistakes-when-using-architecture-components-403e9899f4cb
Okay I've managed to solve this leak. The leak was caused, because I've injected my ShopAdapter via Constructor Injection into my Fragment. When injecting something into the fragment via constructor injection, you have to pass the dependency to the MainFragmentFactory. But because of this, the MainFragmentFactory will always hold a reference to the adapter, even when the fragment is destroyed and the fragment is not needed any more (therefore, requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null wont't even make a change here).
To solve this problem, DON'T inject the Adapter via constructor injection and rather inject it via field injection.