Android paging flowable leaking viewmodel and fragment - android

I'm using the paging 3 android library with the RxJava source. I have two fragments, the first displays a list of images in a grid, when an image is clicked the second fragment is shown and it displays the image in fullscreen and has a ViewPager to swipe between images. Because those use the same data I figured I can use a shared view model, in both fragments I have
#Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by activityViewModels<FilesViewModel> { viewModelFactory }
And the viewmodel creates the rx flowable that both fragments observe when their view is visible
class FilesViewModel #Inject constructor(
settings: SettingsRepository,
private val filesRepository: FilesRepository
): ViewModel() {
...
var cachedFileList = filesRepository.getPagedFiles("path").cachedIn(viewModelScope)
...
}
After navigating back to the list the fragment is retained, after doing that five times LeakCanary shows the leak
┬───
│ GC Root: System class
│
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.guillermonegrete.gallery.folders.MainActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ mApplication instance of com.guillermonegrete.gallery.MyApplication
│ mBase instance of android.app.ContextImpl
│ ↓ ComponentActivity.mViewModelStore
│ ~~~~~~~~~~~~~~~
├─ androidx.lifecycle.ViewModelStore instance
│ Leaking: UNKNOWN
│ Retaining 816 B in 11 objects
│ ↓ ViewModelStore.mMap
│ ~~~~
├─ java.util.HashMap instance
│ Leaking: UNKNOWN
│ Retaining 804 B in 10 objects
│ ↓ HashMap.table
│ ~~~~~
├─ java.util.HashMap$HashMapEntry[] array
│ Leaking: UNKNOWN
│ Retaining 764 B in 9 objects
│ ↓ HashMap$HashMapEntry[].[0]
│ ~~~
├─ java.util.HashMap$HashMapEntry instance
│ Leaking: UNKNOWN
│ Retaining 520 B in 6 objects
│ ↓ HashMap$HashMapEntry.value
│ ~~~~~
├─ com.guillermonegrete.gallery.files.FilesViewModel instance
│ Leaking: UNKNOWN
│ Retaining 4,0 kB in 145 objects
│ ↓ FilesViewModel.cachedFileList
│ ~~~~~~~~~~~~~~
├─ io.reactivex.internal.operators.flowable.FlowableFromPublisher instance
│ Leaking: UNKNOWN
│ Retaining 28 B in 2 objects
│ ↓ FlowableFromPublisher.publisher
│ ~~~~~~~~~
├─ kotlinx.coroutines.reactive.FlowAsPublisher instance
│ Leaking: UNKNOWN
│ Retaining 16 B in 1 objects
│ ↓ FlowAsPublisher.flow
│ ~~~~
├─ kotlinx.coroutines.flow.SafeFlow instance
│ Leaking: UNKNOWN
│ Retaining 48 B in 2 objects
│ ↓ SafeFlow.block
│ ~~~~~
├─ androidx.paging.multicast.Multicaster$flow$1 instance
│ Leaking: UNKNOWN
│ Retaining 36 B in 1 objects
│ Anonymous subclass of kotlin.coroutines.jvm.internal.SuspendLambda
│ ↓ Multicaster$flow$1.this$0
│ ~~~~~~
├─ androidx.paging.multicast.Multicaster instance
│ Leaking: UNKNOWN
│ Retaining 108 B in 4 objects
│ ↓ Multicaster.channelManager$delegate
│ ~~~~~~~~~~~~~~~~~~~~~~~
├─ kotlin.SynchronizedLazyImpl instance
│ Leaking: UNKNOWN
│ Retaining 50 B in 2 objects
│ ↓ SynchronizedLazyImpl._value
│ ~~~~~~
├─ androidx.paging.multicast.ChannelManager instance
│ Leaking: UNKNOWN
│ Retaining 30 B in 1 objects
│ ↓ ChannelManager.actor
│ ~~~~~
├─ androidx.paging.multicast.ChannelManager$Actor instance
│ Leaking: UNKNOWN
│ Retaining 19,5 kB in 573 objects
│ ↓ ChannelManager$Actor.channels
│ ~~~~~~~~
├─ java.util.ArrayList instance
│ Leaking: UNKNOWN
│ Retaining 19,4 kB in 567 objects
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 19,4 kB in 566 objects
│ ↓ Object[].[1]
│ ~~~
├─ androidx.paging.multicast.ChannelManager$ChannelEntry instance
│ Leaking: UNKNOWN
│ Retaining 3,6 kB in 110 objects
│ ↓ ChannelManager$ChannelEntry.channel
│ ~~~~~~~
├─ kotlinx.coroutines.channels.LinkedListChannel instance
│ Leaking: UNKNOWN
│ Retaining 3,6 kB in 109 objects
│ ↓ AbstractSendChannel.queue
│ ~~~~~
├─ kotlinx.coroutines.internal.LockFreeLinkedListHead instance
│ Leaking: UNKNOWN
│ Retaining 3,6 kB in 108 objects
│ ↓ LockFreeLinkedListNode._next
│ ~~~~~
├─ kotlinx.coroutines.channels.Closed instance
│ Leaking: UNKNOWN
│ Retaining 3,6 kB in 106 objects
│ ↓ Closed.closeCause
│ ~~~~~~~~~~
├─ kotlinx.coroutines.JobCancellationException instance
│ Leaking: UNKNOWN
│ Retaining 3,5 kB in 105 objects
│ ↓ JobCancellationException.job
│ ~~~
├─ kotlinx.coroutines.reactive.FlowSubscription instance
│ Leaking: UNKNOWN
│ Retaining 3,4 kB in 102 objects
│ ↓ FlowSubscription.subscriber
│ ~~~~~~~~~~
├─ io.reactivex.internal.operators.flowable.
│ FlowableSubscribeOn$SubscribeOnSubscriber instance
│ Leaking: UNKNOWN
│ Retaining 3,3 kB in 99 objects
│ ↓ FlowableSubscribeOn$SubscribeOnSubscriber.downstream
│ ~~~~~~~~~~
├─ io.reactivex.internal.operators.flowable.
│ FlowableObserveOn$ObserveOnSubscriber instance
│ Leaking: UNKNOWN
│ Retaining 3,2 kB in 93 objects
│ ↓ FlowableObserveOn$ObserveOnSubscriber.downstream
│ ~~~~~~~~~~
├─ io.reactivex.internal.subscribers.LambdaSubscriber instance
│ Leaking: UNKNOWN
│ Retaining 2,6 kB in 86 objects
│ ↓ LambdaSubscriber.onNext
│ ~~~~~~
├─ com.guillermonegrete.gallery.files.details.
│ FileDetailsFragment$setUpViewModel$1 instance
│ Leaking: UNKNOWN
│ Retaining 2,5 kB in 85 objects
│ Anonymous class implementing io.reactivex.functions.Consumer
│ ↓ FileDetailsFragment$setUpViewModel$1.this$0
│ ~~~~~~
╰→ com.guillermonegrete.gallery.files.details.FileDetailsFragment instance
Leaking: YES (ObjectWatcher was watching this because com.
guillermonegrete.gallery.files.details.FileDetailsFragment received
Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
Retaining 2,5 kB in 84 objects
key = 0b0dad5d-1c55-4938-94d8-0f923fc29508
watchDurationMillis = 23025
retainedDurationMillis = 18023
You can see that the 2nd fragment is leaking and it is related to cachedFileList
Now if I remove the cachedIn(viewModelScope) then the leaks are gone however the app now makes API calls every time I navigate between fragments, which the whole point of sharing the view model is to save api calls.
Is there any way to avoid the multiple api calls and the leaks? I know I can use a database but I want to avoid that overhead if possible.
EDIT:
How the flow is consumed, basically the same for both fragments
class FilesListFragment: Fragment(R.layout.fragment_files_list) {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
bindViewModel(folder)
}
private fun bindViewModel(folder: String){
disposable.add(viewModel.loadPagedFiles(folder)
.subscribeOn(Schedulers.io()) // Omitted some mapping
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ adapter.submitData(lifecycle, it) },
{ error -> println("Error loading files: ${error.message}") }
)
)
}
override fun onDestroyView() {
binding.filesList.adapter = null
_binding = null
disposable.clear()
adapter.removeLoadStateListener(loadListener)
super.onDestroyView()
}
...
}
Here is how it is created
class DefaultFilesRepository #Inject constructor(private var fileAPI: FilesServerAPI): FilesRepository {
override fun getPagedFiles(folder: String): Flowable<PagingData<File>> {
return Pager(PagingConfig(pageSize = 20)) {
FilesPageSource(fileAPI, baseUrl, folder)
}.flowable
}
}

As recommended by #dlam, I used a switchMap.
class FilesViewModel #Inject constructor(
settings: SettingsRepository,
private val filesRepository: FilesRepository
): ViewModel() {
...
private val folderName: Subject<String> = PublishSubject.create()
var cachedFileList: Flowable<PagingData<File>> = folderName.distinctUntilChanged().switchMap {
filesRepository.getPagedFiles(it).toObservable()
}.toFlowable(BackpressureStrategy.LATEST).cachedIn(viewModelScope)
fun setFolderName(name: String){
folderName.onNext(name)
}
...
}
And observing the data from the fragments
disposable.add(viewModel.loadPagedFiles(folder)
// .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) // Omitted some mapping
.subscribe(
{ adapter.submitData(lifecycle, it) },
{ error -> println("Error loading files: ${error.message}") }
)
)
viewModel.setFolderName(folder)
I removed .subscribeOn(Schedulers.io()) from the 1st fragment, for some reason it caused the switchMap to never be called at the start. A related question.
Also removed the one in the 2nd fragment. When navigating back to the 1st fragment this exception was thrown:
kotlinx.coroutines.channels.ClosedSendChannelException: Channel was closed
After removing the subscribeOn the exception went away.

Related

Memory Leaking Android

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()
}

ExternalSyntheticLambda1 Leak Canary

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

android Navigation UI Component Memory leak

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%.

What is the difference between Fragment pop back stack and removing fragment using trasaction ? Which is the best way to remove a fragment?

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```

Databinding memory-leak

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

Categories

Resources