Debarshi Bhattacharjee
Debarshi Bhattacharjee

Reputation: 830

Fragment view memory leak despite setting parent view to null in onDestroyView

I know that adding a fragment transaction to the backstack and then moving from that fragment to another fragment, the reference of the previous fragment's view is still available and it only gets destroyed when the back button is pressed. And to avoid this, I have set the view to null in onDestroyView but the problem is, leakcanary still shows view is not null and the view reference is still available whereas logging the view says it is null.

Why is it so ? Also, please correct me if I'm wrong or missing anything.

The fragment class-


private var mView: View? = null
private lateinit var btnSignUp: Button

 override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mView = inflater.inflate(R.layout.fragment_login, container, false)
        return mView
    }

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        btnSignUp = view.findViewById(R.id.btnSignUp)

        btnSignUp.setOnClickListener {
            // calling function changeFragment()
            changeFragment(SignUpFragment(), FragmentsTag.SIGNUP_FRAGMENT)
        }
    }

override fun onDestroyView() {
        super.onDestroyView()      
         mView=null
    }

LeakCanary Analysis logs --

  HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS

    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.

    43817 bytes retained by leaking objects
    Signature: 6e77557c8a679dd41391c1c5badaac98217366ad
    ┬───
    │ GC Root: System class
    │
    ├─ leakcanary.internal.InternalLeakCanary class
    │    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
    │    ↓ static InternalLeakCanary.resumedActivity
    ├─ com.example.foodrunner.activities.MainActivity instance
    │    Leaking: NO (LoginFragment↓ is not leaking and Activity#mDestroyed is false)
    │    ↓ MainActivity.mFragments
    ├─ androidx.fragment.app.FragmentController instance
    │    Leaking: NO (LoginFragment↓ is not leaking)
    │    ↓ FragmentController.mHost
    ├─ androidx.fragment.app.FragmentActivity$HostCallbacks instance
    │    Leaking: NO (LoginFragment↓ is not leaking)
    │    ↓ FragmentActivity$HostCallbacks.mFragmentManager
    ├─ androidx.fragment.app.FragmentManagerImpl instance
    │    Leaking: NO (LoginFragment↓ is not leaking)
    │    ↓ FragmentManagerImpl.mActive
    ├─ java.util.HashMap instance
    │    Leaking: NO (LoginFragment↓ is not leaking)
    │    ↓ HashMap.table
    ├─ java.util.HashMap$HashMapEntry[] array
    │    Leaking: NO (LoginFragment↓ is not leaking)
    │    ↓ HashMap$HashMapEntry[].[0]
    ├─ java.util.HashMap$HashMapEntry instance
    │    Leaking: NO (LoginFragment↓ is not leaking)
    │    ↓ HashMap$HashMapEntry.value
    ├─ com.example.foodrunner.fragments.LoginFragment instance
    │    Leaking: NO (Fragment#mFragmentManager is not null)
    │    Fragment.mTag=Login Fragment
    │    ↓ LoginFragment.btnLogin
    │                    ~~~~~~~~
    ├─ com.google.android.material.button.MaterialButton instance
    │    Leaking: YES (View detached and has parent)
    │    mContext instance of com.example.foodrunner.activities.MainActivity with mDestroyed = false
    │    View#mParent is set
    │    View#mAttachInfo is null (view detached)
    │    View.mID = R.id.btnLogin
    │    View.mWindowAttachCount = 1
    │    ↓ MaterialButton.mParent
    ╰→ androidx.constraintlayout.widget.ConstraintLayout instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.example.foodrunner.fragments.LoginFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
    ​     key = b72a82a6-b9dd-46c6-afb2-0ea6c7025001
    ​     watchDurationMillis = 9582
    ​     retainedDurationMillis = 4582
    ​     key = 0554b63a-c700-4c86-a451-b0daae06607a
    ​     watchDurationMillis = 9581
    ​     retainedDurationMillis = 4580
    ​     mContext instance of com.example.foodrunner.activities.MainActivity with mDestroyed = false
    ​     View#mParent is null
    ​     View#mAttachInfo is null (view detached)
    ​     View.mWindowAttachCount = 1
    ====================================

Upvotes: 9

Views: 7457

Answers (2)

ianhanniballake
ianhanniballake

Reputation: 199825

You're still holding onto a reference to btnSignUp after onDestroyView - that is what is leaking. You have to drop all reference to all Views within the view that was just destroyed.

Therefore you should either use the same approach (make it a nullable var) or not hold onto a reference to btnSignUp in your Fragment at all - at least in your code sample, it could easily be a local variable. (In fact, the same applies to your mView - you get the View as an input to onViewCreated(), there's no reason to hold onto it at the Fragment level).

Upvotes: 9

Karan Khurana
Karan Khurana

Reputation: 585

As @Rafsanjani mention you can use this too :

FragmentManager manager = getActivity().getSupportFragmentManager();
FragmentTransaction trans = manager.beginTransaction();
trans.remove(myFrag);
trans.commit();
manager.popBackStack();

Use this in your onBackPressed

Upvotes: 0

Related Questions