注册

反射解决FragmentDialog内存泄露??‍♂️

怎么引发内存泄露的


这个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引起的内存泄露 来自隔壁的。我看这位老哥最早发布的,不知道老哥是不是原创作者,如果是还是很厉害的。我也是从中学习到了。虽然我的解决办法是从他那里学到的,但是我不会复制粘贴别人的文章,不能做技术的盗窃者。我也不会使用别人的代码,我喜欢自己动手写,这样能在写代码中学到更多东西。


作者:年小个大
链接:https://juejin.cn/post/7012569192251523080
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册