I’ve been busy these past few months. When reviewing my previous code, I found the code that I used to solve the memory leak of DialogFragment. So I did some research and felt like I could share it.

What caused the memory leak

I encountered the memory leak of this DialogFragment several years ago, but I was also confused at that time. I searched various methods on the Internet, but I was also in a fog and confusion. After reviewing a lot of data, I finally understand why it causes memory leaks.

This allows Dialog to dismissListener and setOnCancelListener; this allows Dialog to dismiss the current DialogFragment reference to Message. In complex projects, a variety of third-party libraries have their own message handling associated with a root HandleThread, which can be problematic. (The last sentence I moved, in fact, I do not know 🤣)

In looper.loop (), use messagequyue.next () to fetch the message. If there is no message, next() will remain in a pending state, and MessageQueue will always check if the last message chain has a next message added, so the last message will always be indexed. Until the next Message appears.

I will not show the source code, because it may not understand, so I wrote a simple and similar test based on my own understanding:

I will create my own Looper->MyLooper to simulate Looper operation

object MyLooper {
    // A class that handles message queues
    val myQueue = MyMessageQueue()
    /// Add a message
    fun addMessage(msg: Message) {
        println("Add message:${msg.obj}")
        myQueue.addMessage(msg)
    }
    / / start
    fun lopper(a) {
        while (true) {
            val next = myQueue.next()
            println("Processing messages ---->${next? .obj}")
            if (next == null) {
                return}}}}Copy the code

Create MessageQueue and queue MessageQueue. I’m not going to write that complicated, but almost the same meaning, one is Message carrier, one is Message queue.


class Message(var obj: Any? = null.var next: Message? = null)

class MyMessageQueue {
    // Initial message
    private var message: Message = Message("Thread start")
    // Add the new message to the end of the current message
    fun addMessage(msg: Message?). {
        // My next message will be you
        message.next = msg
    }
    // Retrieve the next Message. If there is no next Message, I wait for the next Message to appear.
    fun next(a): Message {
        while (true) {
            if (message.next == null) {
                println("Recheck message currently stuck message -${message.obj}")
                Thread.sleep(100)
                continue
            }
            val next = message.next
            message = next!!
            return message
        }
    }
}
Copy the code

Try writing a test class

    @Test
    fun test(a) {
        println("Message tests begin")
        Thread {
            MyLooper.lopper()
        }.start()
        Thread.sleep(100)
        MyLooper.addMessage(Message("One Message"))// Send the first message
        Thread.sleep(100)
        MyLooper.addMessage(Message("Two Message"))// Send the second message
        Thread.sleep(100)
        while (true) {
            continue}}Copy the code

The results are as good as expected, and the last message is indexed all the time.

That’s pretty much what I mean.

How to deal with

DialogFragment uses the message mechanism to notify itself that it is closed. This logic cannot be changed. We can only weakly reference the current DialogFragment to let the system recycle it for us when GG. My final solution is to replace the variables of the parent class by reflection.

rewriteDialogFragmentSet two listeners
    private DialogInterface.OnCancelListener mOnCancelListener =
            new DialogInterface.OnCancelListener() {
        @SuppressLint("SyntheticAccessor")
        @Override
        public void onCancel(@Nullable DialogInterface dialog) {
            if(mDialog ! =null) {
                DialogFragment.this.onCancel(mDialog); }}};private DialogInterface.OnDismissListener mOnDismissListener =
            new DialogInterface.OnDismissListener() {
        @SuppressLint("SyntheticAccessor")
        @Override
        public void onDismiss(@Nullable DialogInterface dialog) {
            if(mDialog ! =null) {
                DialogFragment.this.onDismiss(mDialog); }}};Copy the code

DialogFragment = “this”; DialogFragment = “this”;

So let’s override two listeners.

Since the flow of both listeners is pretty much the same, I wrote an interface, as you’ll see.

interface IDialogFragmentReferenceClear {
    // Weak reference object
    val fragmentWeakReference: WeakReference<DialogFragment>
    // Clean up weak references
    fun clear(a)
}
Copy the code

Override cancel listener:

class OnCancelListenerImp(dialogFragment: DialogFragment) :
    DialogInterface.OnCancelListener, IDialogFragmentReferenceClear {

    override val fragmentWeakReference: WeakReference<DialogFragment> =
        WeakReference(dialogFragment)

    override fun onCancel(dialog: DialogInterface) {
        fragmentWeakReference.get()? .onCancel(dialog) }override fun clear(a) {
        fragmentWeakReference.clear()
    }
}
Copy the code

Overwrite close listener:

class OnDismissListenerImp(dialogFragment: DialogFragment) :
    DialogInterface.OnDismissListener, IDialogFragmentReferenceClear {

    override val fragmentWeakReference: WeakReference<DialogFragment> =
        WeakReference(dialogFragment)

    override fun onDismiss(dialog: DialogInterface) {
        fragmentWeakReference.get()? .onDismiss(dialog) }override fun clear(a) {
        fragmentWeakReference.clear()
    }
}
Copy the code

It’s easy.

And then there’s the substitution.

Replace the listener of the parent class

My substitution here is to directly replace the DialogFragment variables.

When we replace the listeners of the parent class, we must replace them before the parent class uses them. Because during my testing, there is still a very small chance of memory leaks after I replace them, and I’m speechless, but I don’t know why.

Let’s start by reviewing the Dialog creation process:

Starting with onCreateDialog(@Nullable Bundle savedInstanceState), you’ll find each of these methods in turn.

  1. public LayoutInflater onGetLayoutInflater
  2. private void prepareDialog
  3. public Dialog onCreateDialog

The above is executed in order of 1.2.3. The Dialog setting listener is triggered in onGetLayoutInflater, so we override this method. Replace before the parent class executes, using reflection to replace ~

    override fun onGetLayoutInflater(savedInstanceState: Bundle?).: LayoutInflater {
        // Try reflection substitution first
        val isReplaceSuccess = replaceCallBackByReflexSuper()
        // Now you can perform the operations of the parent class
        val layoutInflater = super.onGetLayoutInflater(savedInstanceState)
        if(! isReplaceSuccess) { Log.d("Dboy"."Reflection setting DialogFragment failed! Try setting Dialog listener")
            replaceDialogCallBack()
        } else {
            Log.d("Dboy"."Reflection setting DialogFragment succeeded!")}return layoutInflater
    }
Copy the code

Here is the core replacement operation. We find the class and field to replace, and reflection modifies its value.

    private fun replaceCallBackByReflexSuper(a): Boolean {
        try {
            val superclass: Class<*> =
                findSuperclass(javaClass, DialogFragment::class.java) ? :return false
            // Re-assign the cancel interface
            val mOnCancelListener = superclass.getDeclaredField("mOnCancelListener")
            mOnCancelListener.isAccessible = true
            mOnCancelListener.set(this, OnCancelListenerImp(this))
            // Re-assign a value to the closed interface
            val mOnDismissListener = superclass.getDeclaredField("mOnDismissListener")
            mOnDismissListener.isAccessible = true
            mOnDismissListener.set(this, OnDismissListenerImp(this))
            return true
        } catch (e: NoSuchFieldException) {
            Log.e("Dboy"."Dialog reflection replacement failed: variable not found")}catch (e: IllegalAccessException) {
            Log.e("Dboy"."Dialog reflection replacement failed: access not allowed")}return false
    }
Copy the code

After the reflection fetch failed, we manually set it up to look at the call timing above.

    private fun replaceDialogCallBack(a) {
        if (mOnCancelListenerImp == null) {
            mOnCancelListenerImp = OnCancelListenerImp(this) } dialog? .setOnCancelListener(mOnCancelListenerImp)if (mOnDismissListenerImp == null) {
            mOnDismissListenerImp = OnDismissListenerImp(this) } dialog? .setOnDismissListener(mOnDismissListenerImp) }Copy the code

ReplaceDialogCallBack replaces the callback interface, reducing memory leaks, but not completely eliminating them. In no particular case will reflection succeed, as long as the reflection replacement succeeds, so long as memory leaks are bye-bye.

Then onDestroyView will clear our weak references.

    override fun onDestroyView(a) {
        super.onDestroyView()
        // Clean up weak references manuallymOnCancelListenerImp? .clear() mOnCancelListenerImp =nullmOnDismissListenerImp? .clear() mOnDismissListenerImp =null
    }
Copy the code

Why isn’t your solution working

This memory leak has been with me since I first came into contact with Dialog Fragments.

OnCreateDialog = onCreateDialog = onCreateDialog = onCreateDialog = onCreateDialog = onCreateDialog Oh, my god. Now that I think about it it seems like the dumbest way to solve it, to solve it for the sake of solving it, to cut it off.

There is a more reliable way to override the interface weak reference objects as I did, but that method is a reassignment of the interface to the Dialog in onActivityCreated. This method is feasible. But then I realized it wasn’t working. The reason is that there is still a chance of a memory leak after the parent class sets a listener first.

This allows you to dismissListener; this allows you to dismiss Listener and mOnCancelListener. This allows you to dismissListener; this allows you to dismissListener. Actually, I noticed that too.

Why is that?

DialogFragment source package is dependent on appCompat, there are several versions of it.

When you refer to a version lower than 1.3.0 it does not apply to my solution. This is available when you are older than 1.3.0, but you can also separate Fragment dependencies as long as they are older than 1.3.0.

Appcompat: 1.2.0 source

In 1.2.0, only two listeners could be reconfigured in onActivityCreated to reduce the probability of memory leaks

 override fun onActivityCreated(savedInstanceState: Bundle?). {
     super.onActivityCreated(savedInstanceState)
     if (isLowVersion) {
         Log.d("Dboy"."Replace override in lower version")
		if (mOnCancelListenerImp == null) {
            mOnCancelListenerImp = OnCancelListenerImp(this) } dialog? .setOnCancelListener(mOnCancelListenerImp)if (mOnDismissListenerImp == null) {
            mOnDismissListenerImp = OnDismissListenerImp(this) } dialog? .setOnDismissListener(mOnDismissListenerImp) } }Copy the code
Appcompat :1.3.0

There’s a big difference between the two versions. So you directly search for solutions, put into your project, because the version is not correct, resulting in no effect. But I did make an alternative. When reflection fails and the variable is not found, mark it as a lower version and set it again in onActivityCreated.

When you reference a third party library or other module with different versions of AppCompat, the highest version of your project will be used for packaging, so be careful to check for dependency conflicts, too many versions of appCompat content will be reported directly error.

Add confusion

I almost forgot the most important thing, since it is reflection, of course, there is no confusion of files. We only need to ensure that the two variables mOnCancelListener and mOnCancelListener in DialogFragment are not confused during obfuscation compilation.

Add this rule to your project’s ProGuard-rules.pro:

-keepnames class androidx.fragment.app.DialogFragment{
    private ** mOnCancelListener;
    private ** mOnDismissListener;
}
Copy the code

After the speech

When I solved this memory leak, I was really bored to death at that time, searching for posts on the Internet, either copy and paste someone else’s or copy and paste someone else’s. When I see a good post, I go to the original post and I find an article that uses weak references to solve memory leaks caused by DialogFragment from the next door. I see this elder brother the earliest release, do not know elder brother is not the original author, if it is still very powerful. And I learned from it. Although I learned my solution from him, I can’t copy and paste other people’s articles and can’t be a technology thief. I don’t use other people’s code, I like to do it myself and learn more from it.

Well, that’s it, a little grumbling.

I’ve also open-source the code. Interested to have a look, there are explanations and mistakes in the article are also welcome to point out.

Dboy233/DialogFragment(github.com)

Reference article:

  • A small leak will sink a great shipThe foreigner forHandleThreadExplanation of the memory leak
  • DialogFragment memory leak caused by these two same, the originator is different
  • DialogFragment memory leak caused by these two same, the originator is different