反射解决DialogFragment内存泄露👌🤷‍♂️

·  阅读 2659
反射解决DialogFragment内存泄露👌🤷‍♂️

这几个月一直在忙。在审自己以前的代码的时候发现了自己当初解决DialogFragment内存泄露的代码。于是我有研究了一番,感觉可以分享一下。

怎么引发内存泄露的

这个DialogFragment的内存泄露几年前我就遇到了,但当时也稀里糊涂的,在网上搜索各种办法,看的我也是云里雾里,迷迷糊糊。在查阅大量资料之后,终于明白为什么会导致内存泄露了。

归根到底就是DialogFragment在给Dialog设置setOnCancelListenersetOnDismissListener的时候将当前的DialogFragment引用传给了Message。在一些复杂项目中,各种各样的第三方库都有自己的消息处理,是根HandleThread有关系,这玩意一多就容易有问题。(最后一句话我搬的,其实我也不清楚🤣)

Looper.loop()中用MessageQueue.next()去取消息,如果之后没有消息,next()会处于一个挂起状态,MessageQueue会一直检测最后一条消息链是否有next消息被添加,于是最后的消息会被一直索引,直到下一条Message出现。

我就不展示这些源码了,因为可能看不懂,所以我根据自己的理解写了个简单的差不多的测试:

我先创建一个自己的Looper->MyLooper,模拟Looper的运作

object MyLooper {
    //处理消息队列的类
    val myQueue = MyMessageQueue()
    ///添加一条消息
    fun addMessage(msg: Message) {
        println("添加消息: ${msg.obj}")
        myQueue.addMessage(msg)
    }
    //开始吧
    fun lopper() {
        while (true) {
            val next = myQueue.next()
            println("处理消息---->${next?.obj}")
            if (next == null) {
                return
            }
        }
    }
}

创建消息Message和队列MessageQueue,我不写那么复杂了,差不多一个意思,一个是消息载体,一个是处理消息队列的。


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

class MyMessageQueue {
    //初始消息
    private var message: Message = Message("线程启动")
    //将新来的消息添加到当前消息的屁股后面
    fun addMessage(msg: Message?) {
        //我的下一个消息就是你
        message.next = msg
    }
    //检索下一个Message,如果没有下一个message,我就等下一条消息出现。
    fun next(): Message {
        while (true) {
            if (message.next == null) {
                println("重新检查消息 当前被卡住的消息-${message.obj}")
                Thread.sleep(100)
                continue
            }
            val next = message.next
            message = next!!
            return message
        }
    }
}

写一个测试类试试

    @Test
    fun test() {
        println("消息测试开始")
        Thread {
            MyLooper.lopper()
        }.start()
        Thread.sleep(100)
        MyLooper.addMessage(Message("One Message"))//发送第一个消息
        Thread.sleep(100)
        MyLooper.addMessage(Message("Two Message"))//发送第二个消息
        Thread.sleep(100)
        while (true) {
            continue
        }
    }

运行结果也不负众望,最后一条消息一直被索引。

myLooper.png

这差不多就是我理解的意思。

如何处理

DialogFragment要通过消息机制来通知自己关闭了,这个逻辑没办法更改。我们只能通过弱引用当前的DialogFragment让系统GG的时候帮我们回收掉,我的最终解决是通过反射替换父类的变量。

重写DialogFragment设置的两个监听器
    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);
            }
        }
    };

上面两个是DialogFragment源码的两个监听器,不管他怎么写,最后都是要把当前的this放进去。

所以我们重写两个监听器。

因为两个监听器的操作流程差不多一样,我就写了个接口,等会你就明白了。

interface IDialogFragmentReferenceClear {
    //弱引用对象
    val fragmentWeakReference: WeakReference<DialogFragment>
    //清理弱引用
    fun clear()
}

重写取消监听器:

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() {
        fragmentWeakReference.clear()
    }
}

重写关闭监听器:

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() {
        fragmentWeakReference.clear()
    }
}

很简单是吧。

然后就是替换了。

替换父类的监听器

我这里的替换是直接替换的DialogFragment这两个变量。

我们在替换父类的监听器的时候,一定要在父类使用这两个监听器之前替换。因为在我测试过程中,在之后替换,还是有极小的概率造成内存泄露,很无语,但我也不知道为什么。

我们先捋一下Dialog的创建流程:

onCreateDialog(@Nullable Bundle savedInstanceState)出发,会依次找到这几个方法。

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

上面是按1.2.3顺序执行的。触发Dialog设置监听器是在onGetLayoutInflater,所以我们重写这个方法。在父类执行之前进行替换,使用反射替换~

    override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater {
        //先尝试反射替换
        val isReplaceSuccess = replaceCallBackByReflexSuper()
        //现在可以执行父类的操作了
        val layoutInflater = super.onGetLayoutInflater(savedInstanceState)
        if (!isReplaceSuccess) {
            Log.d("Dboy", "反射设置DialogFragment 失败!尝试设置Dialog监听")
            replaceDialogCallBack()
        } else {
            Log.d("Dboy", "反射设置DialogFragment 成功!")
        }
        
        return layoutInflater
    }

这里是核心的替换操作。我们找到要替换的类和字段,然后反射修改它的值。

    private fun replaceCallBackByReflexSuper(): Boolean {
        try {
            val superclass: Class<*> =
                findSuperclass(javaClass, DialogFragment::class.java) ?: return false
            //重新给取消接口赋值
            val mOnCancelListener = superclass.getDeclaredField("mOnCancelListener")
            mOnCancelListener.isAccessible = true
            mOnCancelListener.set(this, OnCancelListenerImp(this))
            //重新给关闭接口赋值
            val mOnDismissListener = superclass.getDeclaredField("mOnDismissListener")
            mOnDismissListener.isAccessible = true
            mOnDismissListener.set(this, OnDismissListenerImp(this))
            return true
        } catch (e: NoSuchFieldException) {
            Log.e("Dboy", "dialog 反射替换失败:未找到变量")
        } catch (e: IllegalAccessException) {
            Log.e("Dboy", "dialog 反射替换失败:不允许访问")
        }
        return false
    }

我们在反射获取失败之后,在手动进行一次设置,看上面的调用时机。

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

replaceDialogCallBack替换回调接口,可以减少内存泄露,但不能完全解决内存泄露。在没有特殊情况下,反射都是会成功的,只要反射替换成功,给内存泄露说拜拜。

然后再onDestroyView清空一下我们的弱引用。

    override fun onDestroyView() {
        super.onDestroyView()
        //手动清理一下弱引用
        mOnCancelListenerImp?.clear()
        mOnCancelListenerImp = null

        mOnDismissListenerImp?.clear()
        mOnDismissListenerImp = null
    }

为什么你的解决方法不管用

我刚接触DialogFragment的时候,这个内存泄露就一直伴随着我。

我当时菜鸟,在网上找各种解决方法,有的说重写onCreateDialog替换一个自己的Dialog,重写两个监听器设置方法,然后不让DialogFragment设置这两个监听器就解决了...我去,我现在想想感觉这个是最弱智的解决办法了,完全是为了解决而解决,直接掐断源头。

之后还有一个比较靠谱的方法,和我这个一样,也是重写这两个接口弱引用对象,不过那个方法是在onActivityCreated中对Dialog的这两个接口进行的重新赋值。这个方法是可行了。但是后来,我发现又不行了。就是因为是在父类先设置一次监听器之后还是有机会造成内存泄露。

还有就是说,等你去翻阅自己AndroidStudio的DialogFragment源码之后你会发现你根本没有看到父类有这两个变量mOnCancelListenermOnDismissListener。其实我也发现了。

这是为什么?

DialogFragment的源码包是依赖在appcompat中的,它的版本有好几个.

appcompat_versions.png

当你引用低于1.3.0的版本是不适用于我这个解决办法的。当你高于1.3.0版本是可以使用的,当然你也可以单独引Fragment的依赖只要高于1.3.0就行。

appcompat:1.2.0 的源码

Snipaste_2021-09-27_18-04-50.png

在1.2.0,只能在onActivityCreated中重新设置两个监听器来减少内存泄露出现的概率

 override fun onActivityCreated(savedInstanceState: Bundle?) {
     super.onActivityCreated(savedInstanceState)
     if (isLowVersion) {
         Log.d("Dboy", "低版本中重新替换覆盖")
		if (mOnCancelListenerImp == null) {
            mOnCancelListenerImp = OnCancelListenerImp(this)
        }
        dialog?.setOnCancelListener(mOnCancelListenerImp)
        
        if (mOnDismissListenerImp == null) {
            mOnDismissListenerImp = OnDismissListenerImp(this)
        }
        dialog?.setOnDismissListener(mOnDismissListenerImp)
     }
 }
appcompat:1.3.0 的源码:

dialogFragment_1.3.4_listener.png dialogFragment_1.3.4_listener_set.png

这两个版本的差异还是比较大的。所以你直接搜的解决办法,放到你的项目里,可能因为版本不对,导致没有效果。不过我也做了替代方案。当反射失败提示找不到变量的时候,做一下标记,认为是低版本,然后再到onActivityCreated中进行一次设置。

当你引用的第三方库或者其他模块中存在不同appcompat版本的时候,打包时会使用你项目里最高版本的,所以要多注意检查是否存在依赖冲突,版本内容差异过大会直接报错的。

加一下混淆

差点忘了最重要的,既然是反射,当然少不了混淆文件了。我们只需要保证在混淆编译的时候,DialogFragment中这两个变量mOnCancelListenermOnCancelListener不被混淆就可以了。

在你项目的proguard-rules.pro中加入这个规则:

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

后言

在我解决这个内存泄露的时候,当时真的是烦死我了,在网上搜索的帖子,不是复制粘贴别人的就是复制粘贴别人的。我看到某个帖子不错之后就会去找原文,我找到一篇使用弱引用解决内存泄露的文章DialogFragment引起的内存泄露 来自隔壁的。我看这位老哥最早发布的,不知道老哥是不是原创作者,如果是还是很厉害的。我也是从中学习到了。虽然我的解决办法是从他那里学到的,但是我不会复制粘贴别人的文章,不能做技术的盗窃者。我也不会使用别人的代码,我喜欢自己动手写,这样能在写代码中学到更多东西。

好了就这样吧,发了点牢骚。。

代码我也开源了。有兴趣去看看,文章中有解释有错误的地方也欢迎指出。

开源地址:Dboy233/DialogFragment(github.com)

参考文章:

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改