LeakCanary DialogFragment leak detection
Asked Answered
A

4

7

Everytime I try to show DialogFragment I get memory leaks.

This is how my test dialog (taken from android developers page) looks like:

public class TestDialog extends DialogFragment {

    public static TestDialog newInstance(int title) {
        TestDialog frag = new TestDialog();
        Bundle args = new Bundle();
        args.putInt("title", title);
        frag.setArguments(args);
        return frag;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        int title = getArguments().getInt("title");

        return new AlertDialog.Builder(getActivity())
                .setIcon(R.drawable.ic_action_about)
                .setTitle(title)
                .setPositiveButton(R.string.ok,
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int whichButton) {
                                //((FragmentAlertDialog)getActivity()).doPositiveClick();
                            }
                        }
                )
                .setNegativeButton(R.string.cancel,
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int whichButton) {
                                //((FragmentAlertDialog)getActivity()).doNegativeClick();
                            }
                        }
                )
                .create();
    }
}

I launch it with following code which is executed on button press:

DialogFragment newFragment = TestDialog.newInstance(R.string.company_title);
newFragment.show(getFragmentManager(), "dialog");

And here's the best part: enter image description here

How to solve this leak (or atleast hide it, because canaryleak is getting really annoying with all those notifications)?

Antedate answered 7/11, 2018 at 7:33 Comment(2)
is the code reside in a runnable that you post? if so I think you can ignore this.Pepsin
I'm not sure I follow. It's just a normal setOnClickListener on a Button.Antedate
M
5

The reason this leak caused is the source code of DialogFragment:

@Override
    public void onActivityCreated(Bundle savedInstanceState) {
        ...
        // other codes
        ...
        mDialog.setCancelable(mCancelable);
        // hear is the main reason
        mDialog.setOnCancelListener(this);
        mDialog.setOnDismissListener(this);
        ...
        // other codes
        ...
    }

Let's see what happened in function Dialog.SetOnCancelListener(DialogInterface.OnCancelListener):

/**
     * Set a listener to be invoked when the dialog is canceled.
     *
     * <p>This will only be invoked when the dialog is canceled.
     * Cancel events alone will not capture all ways that
     * the dialog might be dismissed. If the creator needs
     * to know when a dialog is dismissed in general, use
     * {@link #setOnDismissListener}.</p>
     * 
     * @param listener The {@link DialogInterface.OnCancelListener} to use.
     */
    public void setOnCancelListener(@Nullable OnCancelListener listener) {
        if (mCancelAndDismissTaken != null) {
            throw new IllegalStateException(
                    "OnCancelListener is already taken by "
                    + mCancelAndDismissTaken + " and can not be replaced.");
        }
        if (listener != null) {
            // here
            mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener);
        } else {
            mCancelMessage = null;
        }
    }

And, here is the source code of Handler.obtainMessage(int, Object):

    /**
     * 
     * Same as {@link #obtainMessage()}, except that it also sets the what and obj members 
     * of the returned Message.
     * 
     * @param what Value to assign to the returned Message.what field.
     * @param obj Value to assign to the returned Message.obj field.
     * @return A Message from the global message pool.
     */
    public final Message obtainMessage(int what, Object obj)
    {
        return Message.obtain(this, what, obj);
    }

Finally, function Message.obtain(Handler, int, Object) will be called:

    /**
     * Same as {@link #obtain()}, but sets the values of the <em>target</em>, <em>what</em>, and <em>obj</em>
     * members.
     * @param h  The <em>target</em> value to set.
     * @param what  The <em>what</em> value to set.
     * @param obj  The <em>object</em> method to set.
     * @return  A Message object from the global pool.
     */
    public static Message obtain(Handler h, int what, Object obj) {
        Message m = obtain();
        m.target = h;
        m.what = what;
        m.obj = obj;

        return m;
    }

We can see that cancelMessage holds the instance of DialogFragment, which leads to memory leak. I just want to let you know this, and I have no way to avoid it except DO NOT USE DialogFragment. Or someone who has better solutions please let me know.

Musjid answered 6/2, 2020 at 10:59 Comment(0)
B
2

In case someone still bumps into this issue: I fixed this by updating leakcanary to the latest version (2.4 at this point). Seems like it was a false-positive detection. I was using leakcanary 2.0beta-3.

Bridewell answered 14/6, 2020 at 0:22 Comment(0)
A
0

Based on the answer by @EmMper. Here is a work around if you don't need the onCancelListener.

import android.app.Activity
import android.os.Bundle
import androidx.annotation.MainThread
import androidx.fragment.app.DialogFragment

open class PatchedDialogFragment : DialogFragment() {

    @MainThread
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        // Fixing the issue described here
        // https://mcmap.net/q/1508697/-leakcanary-dialogfragment-leak-detection
        val initialShowsDialog = showsDialog
        showsDialog = false
        super.onActivityCreated(savedInstanceState)
        showsDialog = initialShowsDialog

        if (!showsDialog) {
            return
        }
        val view = view
        if (view != null) {
            check(view.parent == null) { "DialogFragment can not be attached to a container view" }
            dialog!!.setContentView(view)
        }
        val activity: Activity? = activity
        if (activity != null) {
            dialog!!.ownerActivity = activity
        }
        dialog!!.setCancelable(isCancelable)
        if (savedInstanceState != null) {
            val dialogState =
                savedInstanceState.getBundle("android:savedDialogState")
            if (dialogState != null) {
                dialog!!.onRestoreInstanceState(dialogState)
            }
        }
    }
}

Just extend PatchedDialogFragment instead of DialogFragment.

Amphicoelous answered 20/6, 2020 at 3:1 Comment(0)
K
0

I got rid of this Leak by removing both OnDismissListener and OnCancelListener from my custom DialogFragment implementation. I also had to pass null to the negative button listener: .setNegativeButton(R.string.cancel, null).

Kilovolt answered 20/6, 2020 at 13:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.