Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 373
Debarshi Bhattacharjee Avatar asked Mar 13 '20 14:03

Debarshi Bhattacharjee


1 Answers

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

like image 183
ianhanniballake Avatar answered Nov 15 '22 07:11

ianhanniballake