反射解决FragmentDialog内存泄露??♂️
怎么引发内存泄露的
这个DialogFragment
的内存泄露几年前我就遇到了,但当时也稀里糊涂的,在网上搜索各种办法,看的我也是云里雾里,迷迷糊糊。在查阅大量资料之后,终于明白为什么会导致内存泄露了。
归根到底就是DialogFragment
在给Dialog
设置setOnCancelListener
和setOnDismissListener
的时候将当前的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
}
}
运行结果也不负众望,最后一条消息一直被索引。
这差不多就是我理解的意思。
如何处理
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)
出发,会依次找到这几个方法。
public LayoutInflater onGetLayoutInflater
private void prepareDialog
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
源码之后你会发现你根本没有看到父类有这两个变量mOnCancelListener
和mOnDismissListener
。其实我也发现了。
这是为什么?
DialogFragment
的源码包是依赖在appcompat
中的,它的版本有好几个.
当你引用低于1.3.0的版本是不适用于我这个解决办法的。当你高于1.3.0版本是可以使用的,当然你也可以单独引Fragment
的依赖只要高于1.3.0
就行。
appcompat:1.2.0 的源码
在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 的源码:
这两个版本的差异还是比较大的。所以你直接搜的解决办法,放到你的项目里,可能因为版本不对,导致没有效果。不过我也做了替代方案。当反射失败提示找不到变量的时候,做一下标记,认为是低版本,然后再到onActivityCreated
中进行一次设置。
当你引用的第三方库或者其他模块中存在不同appcompat
版本的时候,打包时会使用你项目里最高版本的,所以要多注意检查是否存在依赖冲突,版本内容差异过大会直接报错的。
加一下混淆
差点忘了最重要的,既然是反射,当然少不了混淆文件了。我们只需要保证在混淆编译的时候,DialogFragment
中这两个变量mOnCancelListener
、mOnCancelListener
不被混淆就可以了。
在你项目的proguard-rules.pro
中加入这个规则:
-keepnames class androidx.fragment.app.DialogFragment{
private ** mOnCancelListener;
private ** mOnDismissListener;
}
后言
在我解决这个内存泄露的时候,当时真的是烦死我了,在网上搜索的帖子,不是复制粘贴别人的就是复制粘贴别人的。我看到某个帖子不错之后就会去找原文,我找到一篇使用弱引用解决内存泄露的文章DialogFragment引起的内存泄露 来自隔壁的。我看这位老哥最早发布的,不知道老哥是不是原创作者,如果是还是很厉害的。我也是从中学习到了。虽然我的解决办法是从他那里学到的,但是我不会复制粘贴别人的文章,不能做技术的盗窃者。我也不会使用别人的代码,我喜欢自己动手写,这样能在写代码中学到更多东西。
作者:年小个大
链接:https://juejin.cn/post/7012569192251523080
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。