I am trying the new version of GoogleMap
com.google.android.libraries.maps:maps:3.1.0-beta
But it seems to leak memory when I come back to my previous Activity
I'm using Fragment inside my Activity
Here is the code:
XML
<androidx.fragment.app.FragmentContainerView
android:id="#+id/map"
android:name="com.google.android.libraries.maps.SupportMapFragment"
android:layout_width="0dp"
android:layout_height="220dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:useViewLifecycle="true"
tools:context=".OutputScreen"/>
Java Code
public class OutputScreen extends FragmentActivity implements OnMapReadyCallback{
private GoogleMap map;
private SupportMapFragment mapFragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setting layout
mapFragment = (SupportMapFragment)
getSupportFragmentManager().findFragmentById(R.id.map);
mapFragment.getMapAsync(this);
}
#Override
public void onMapReady(GoogleMap googleMap) {
map = googleMap;
}
#Override
protected void onDestroy() {
if (map != null){
map.clear();
map.setMyLocationEnabled(false)
map = null;
}
}
}
LeakCanary Report
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ Leaking: NO (Application↓ is not leaking and a class is never leaking)
│ ↓ static FontsContract.sContext
├─ android.app.Application instance
│ Leaking: NO (Application is a singleton)
│ Application does not wrap an activity context
│ ↓ Application.mActivityLifecycleCallbacks
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
├─ java.util.ArrayList instance
│ Leaking: UNKNOWN
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ ↓ Object[].[5]
│ ~~~
├─ com.google.android.libraries.maps.dj.zzf instance
│ Leaking: UNKNOWN
│ ↓ zzf.zza
│ ~~~
├─ com.google.android.libraries.maps.dj.zzd instance
│ Leaking: UNKNOWN
│ ↓ zzd.zzd
│ ~~~
├─ com.google.android.libraries.maps.bm.zzu instance
│ Leaking: UNKNOWN
│ ↓ zzu.zzd
│ ~~~
├─ com.google.android.libraries.maps.kf.zzab instance
│ Leaking: UNKNOWN
│ ↓ zzab.zzh
│ ~~~
├─ com.google.android.libraries.maps.kf.zzd instance
│ Leaking: UNKNOWN
│ ↓ zzd.zzi
│ ~~~
├─ com.google.android.libraries.maps.ed.zzau instance
│ Leaking: YES (View detached and has parent)
│ mContext instance of android.app.Application, not wrapping activity
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ ↓ zzau.mParent
├─ android.widget.FrameLayout instance
│ Leaking: YES (zzau↑ is leaking and View detached and has parent)
│ mContext instance of android.app.Application, not wrapping activity
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ ↓ FrameLayout.mParent
╰→ android.widget.FrameLayout instance
Leaking: YES (ObjectWatcher was watching this because com.google.android.libraries.maps.SupportMapFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks) and View.mContext references a destroyed activity)
I looked other stackoverflow answers but didn't found any proper solution.
The issue is that the SupportMapFragment is incorporated into your Fragment container view, but the ondestroy is not being called. You need to manually invoke this.
In your OutputScreen class under the onDestroy() method, you need to call the following:
mapFragment.onDestroy() // call this in your activity's onDestroy
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 have this leaked dump from leack canary but somehow I dont understand on how to resolve this.
┬───
│ 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 (InputMethodManager is a singleton)
│ ↓ InputMethodManager.mCurrentInputConnection
│ ~~~~~~~~~~~~~~~~~~~~~~~
├─ com.android.internal.widget.EditableInputConnection instance
│ Leaking: UNKNOWN
│ ↓ EditableInputConnection.mTargetView
│ ~~~~~~~~~~~
├─ androidx.appcompat.widget.AppCompatEditText instance
│ Leaking: YES (View detached and has parent)
│ mContext instance of .android.SIActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.name
│ View.mWindowAttachCount = 1
│ ↓ AppCompatEditText.mParent
├─ androidx.constraintlayout.widget.ConstraintLayout instance
│ Leaking: YES (AppCompatEditText↑ is leaking and View detached and has parent)
│ mContext instance of .android.SIActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.containerName
│ View.mWindowAttachCount = 1
│ ↓ ConstraintLayout.mParent
├─ androidx.constraintlayout.widget.ConstraintLayout instance
│ Leaking: YES (ConstraintLayout↑ is leaking and View detached and has parent)
│ mContext instance of .android.SIActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ ↓ ConstraintLayout.mParent
╰→ androidx.core.widget.NestedScrollView instance
Leaking: YES (ObjectWatcher was watching this because .android.si.SILeadInfoFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
key = a7452263-5234-4656-9425-754c455f1b06
watchDurationMillis = 11855
retainedDurationMillis = 6853
mContext instance of .android.SIActivity with mDestroyed = false
View#mParent is null
View#mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
Some background on the apps:
it is using navigation component from jetpack
lots of form processing inside
Any idea much appreciated, thank you !
This question seem old but I ran into this time to time so here is what I know so far:
I have not found any solution for this except using reflection.
LeakCanary 2.6 has marked this as a library leak (of course it is still a leak that could crash your app due to the OutOfMemory exception).
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 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```
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