Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FragmentStatePagerAdapter memory leak (nested fragments with viewpager)

I have a "memory leak" in my adapter (quotes will be explained later). I'm currently using nested fragments to host a viewpager.

My setup is as following:
1. Activity (Empty activity that hosts Fragment A)
2. Fragment A - fragment that hosts viewpager with Fragmentstatepageradapter. Each viewpager page hosts fragment B.
3. Fragment B - a fragment that contains an imageview.

Everything is working great except when a configuration change occurs. Monitoring the heap, it seems to grow by 100 kb everytime a rotation occurs. Manually GCing does not release the memory.

Things I've tried:
1. Replace Fragment B with a blank fragment - same issue occurs so it's not the imageview that's causing the issue.
2. Remove both fragment A and B and rotate the activity. No memory leak occurs so it's not the activity.
3. Used MAT before any orientation changes and after rotating about 50 times to get the heap up. MAT shows 1 major suspect which is my adapter class. It shows 7MB of retained heap (very small shallow heap) of observers like so:

array java.util.ArrayList @ 0x42079938 24 7,000,832 
.\mObservers android.database.DataSetObservable @ 0x42053508 16 7,000,848 
..\mObservable com.example.main.Adapter@ 0x4205a048 40 7,001,416 

Why I'm using a viewpager inside a fragment:
1. I want to keep the state of the adapter and other variables related to the viewpager alive by setting setretaininstance(true).
2. After a config change, I don't recreate the adapter but use the old one to attach to the viewpager.
3. If I don't reuse the old adapter but create a new adapter after a configuration change, the memory leak dissapears.
4. the memory leak also dissapears after i close the activity and return to the previous activity.

Any ideas? Would appreciate any help.

Thanks, JC

like image 771
JC. Avatar asked Dec 30 '13 10:12

JC.


2 Answers

I had a similar issue, I used ViewPager2, and was in need to use getChildFragmentManager() instead of getSupportFragmentManager() because I want the PageFragment to refer back to the parent fragment (which hosts the ViewPager) with requireParentFragment()

N.B. When I used getSupportFragmentManager() then I got java.lang.IllegalStateException Fragment PageFragment{f152edf} (af30cf2b-acf1-4930-9b83-03ac8144cfc6) f49} is not a child Fragment

The other reason I used getChildFragmentManager(), because I'm using Navigation components, so I don't need to manage the the ViewPager child fragments with the same fragment manager of the navigation components which is getSupportFragmentManager().

Here's the LeakCanary log

2020-09-15 05:39:33.461 9611-9689/.... D/LeakCanary: ┬───
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │ GC Root: Local variable in native code
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: ├─ android.os.HandlerThread instance
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    Leaking: NO (PathClassLoader↓ is not leaking)
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    Thread name: 'LeakCanary-Heap-Dump'
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    ↓ HandlerThread.contextClassLoader
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: ├─ dalvik.system.PathClassLoader instance
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    ↓ PathClassLoader.runtimeInternalObjects
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: ├─ java.lang.Object[] array
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    Leaking: NO (InternalLeakCanary↓ is not leaking)
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    ↓ Object[].[597]
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: ├─ leakcanary.internal.InternalLeakCanary class
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    ↓ static InternalLeakCanary.resumedActivity
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: ├─ .....ui.MainActivity instance
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    Leaking: NO (Activity#mDestroyed is false)
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    ↓ MainActivity.mLifecycleRegistry
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │                   ~~~~~~~~~~~~~~~~~~
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: ├─ androidx.lifecycle.LifecycleRegistry instance
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    Leaking: UNKNOWN
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    ↓ LifecycleRegistry.mObserverMap
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │                        ~~~~~~~~~~~~
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: ├─ androidx.arch.core.internal.FastSafeIterableMap instance
2020-09-15 05:39:33.462 9611-9689/.... D/LeakCanary: │    Leaking: UNKNOWN
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    ↓ FastSafeIterableMap.mHashMap
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │                          ~~~~~~~~
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: ├─ java.util.HashMap instance
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    Leaking: UNKNOWN
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    ↓ HashMap.table
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │              ~~~~~
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: ├─ java.util.HashMap$Node[] array
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    Leaking: UNKNOWN
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    ↓ HashMap$Node[].[5]
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │                     ~~~
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: ├─ java.util.HashMap$Node instance
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    Leaking: UNKNOWN
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    ↓ HashMap$Node.key
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │                   ~~~
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: ├─ androidx.viewpager2.adapter.FragmentStateAdapter$FragmentMaxLifecycleEnforcer$3 instance
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    Leaking: UNKNOWN
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    Anonymous class implementing androidx.lifecycle.LifecycleEventObserver
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    ↓ FragmentStateAdapter$FragmentMaxLifecycleEnforcer$3.this$1
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │                                                          ~~~~~~
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: ├─ androidx.viewpager2.adapter.FragmentStateAdapter$FragmentMaxLifecycleEnforcer instance
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    Leaking: UNKNOWN
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │    ↓ FragmentStateAdapter$FragmentMaxLifecycleEnforcer.mViewPager
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: │                                                        ~~~~~~~~~~
2020-09-15 05:39:33.463 9611-9689/.... D/LeakCanary: ├─ androidx.viewpager2.widget.ViewPager2 instance
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    Leaking: YES (View detached and has parent)
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    mContext instance of .....ui.MainActivity with mDestroyed = false
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    View#mParent is set
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    View#mAttachInfo is null (view detached)
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    View.mID = R.id.viewpager
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    View.mWindowAttachCount = 1
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    ↓ ViewPager2.mParent
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: ├─ androidx.coordinatorlayout.widget.CoordinatorLayout instance
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    Leaking: YES (ViewPager2↑ is leaking and View detached and has parent)
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    mContext instance of .....ui.MainActivity with mDestroyed = false
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    View#mParent is set
2020-09-15 05:39:33.465 9611-9689/.... D/LeakCanary: │    View#mAttachInfo is null (view detached)
2020-09-15 05:39:33.466 9611-9689/.... D/LeakCanary: │    View.mWindowAttachCount = 1
2020-09-15 05:39:33.466 9611-9689/.... D/LeakCanary: │    ↓ CoordinatorLayout.mParent
2020-09-15 05:39:33.466 9611-9689/.... D/LeakCanary: ╰→ androidx.drawerlayout.widget.DrawerLayout instance
2020-09-15 05:39:33.466 9611-9689/.... D/LeakCanary: ​     Leaking: YES (ObjectWatcher was watching this because .....ui.fragments.ReadFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
2020-09-15 05:39:33.466 9611-9689/.... D/LeakCanary: ​     key = e3b96092-06d5-48ae-93bf-b38680cc0c35
2020-09-15 05:39:33.466 9611-9689/.... D/LeakCanary: ​     watchDurationMillis = 6413
2020-09-15 05:39:33.466 9611-9689/.... D/LeakCanary: ​     retainedDurationMillis = 1400
2020-09-15 05:39:33.466 9611-9689/.... D/LeakCanary: ​     mContext instance of .....ui.MainActivity with mDestroyed = false
2020-09-15 05:39:33.467 9611-9689/.... D/LeakCanary: ​     View#mParent is null
2020-09-15 05:39:33.467 9611-9689/.... D/LeakCanary: ​     View#mAttachInfo is null (view detached)
2020-09-15 05:39:33.467 9611-9689/.... D/LeakCanary: ​     View.mID = R.id.drawer_layout

So, to solve this, I cleared any child fragment that has been off the ViewPager; to do this I had to register all fragment Ids, and override containsItem() to clear them in there like below:

public class PageFragmentPagerAdapter extends FragmentStateAdapter {

    private FragmentManager mFragmentMgr;
    private List<Integer> currentPageIds = new ArrayList<>();

    public PageFragmentPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) {
        super(fragmentManager, lifecycle);
        mFragmentMgr = fragmentManager;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        PageFragment pageFragment = PageFragment.newInstance(position);
        currentPageIds.add(position);
        return pageFragment;
    }


    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public int getItemCount() {
        return N_PAGES;
    }

    @Override
    public boolean containsItem(long itemId) {
        for (Integer id : currentPageIds)
            if (id == itemId) {
                currentPageIds.remove(Integer.valueOf(String.valueOf(itemId)));
                clearFragment(id);
                break;
            }
        return super.containsItem(itemId);
    }

    private void clearFragment(int fragmentId) {
        FragmentTransaction transaction = mFragmentMgr.beginTransaction();
        PageFragment fragment = (PageFragment) mFragmentMgr.findFragmentByTag("f" + fragmentId);
        if (fragment != null) {
            transaction.remove(fragment);
        }
        transaction.commitAllowingStateLoss();
    }

}

The second thing don't instantiate the ViewPager adapter with the lifecycle of the activity with requireActivity().getLifecycle(), instead use the lifecycle of its fragment getViewLifecycleOwner().getLifecycle() like below

PageFragmentPagerAdapter mPagerAdapter = new PageFragmentPagerAdapter(
     getChildFragmentManager(), getViewLifecycleOwner().getLifecycle());
like image 106
Zain Avatar answered Oct 02 '22 03:10

Zain


I had a similar memory leak which is now resolved.

In my corresponding Fragment A, I was instantiating the FragmentStatePagerAdapter with this.getFragmentManager() instead of this.getChildFragmentManager() since nested fragments are in place.

Please let me know if this also fixes your issue.

like image 22
Carlos Castro Avatar answered Oct 02 '22 02:10

Carlos Castro