由单例模式引起的内存泄漏

转载注明出处:https://blog.csdn.net/skysukai

1、背景

项目中部署了leakcanary,用于检测app的内存泄漏情况,不知道leakcanary的同学可以自行百度。其中一处泄漏让人印象深刻,提笔记录一下。

2、场景复现

从主界面MainActivity点击进入收藏界面FavoriteFragment,按一次返回键返回主界面MainActivity后再按一次返回键退出app,leakcanary捕捉到内存泄漏。(补充说明:FavoriteFragment里面有个listview用以显示收藏,这个listview就是FavoriteList)

public FavoriteList(Context context, ListView list, FavoriteDatabaseListener listener,
            FileIconHelper iconHelper,Handler handler) {
        mContext = context;
        mFavoriteDatabase = new FavoriteDatabaseHelper(context, this);
        mFavoriteDatabase.setListener(listener);
        ……
    }

leakcanary提示mFavoriteDatabase持有了MainActivity的context,导致内存泄漏。通常来说,有关数据库的操作我们都会用单例模式来维护以节省开销。在这个场景下,mFavoriteDatabase是单例模式维护,它的生命周期和application的生命周期一致,退出app后,系统会回收MainActivity此时application/mFavoriteDatabase没有被回收。而mFavoriteDatabase持有MainActivity的引用,导致MainActivity不能被GC回收——内存泄漏。

3、解决方案

1、手动设置对象为null

如果是由于mFavoriteDatabase持有MainActivity的引用,那最简单的改法就是在MainActivity的onDestory()方法里手动置FavoriteList为null,这样mFavoriteDatabase和MainActivity就不会存在引用关系了:

@Override
    protected void onDestroy() {
        super.onDestroy();
        mFavoriteList = null;
    }

这样改有效吗?
再次场景复现时,leakcanary提示依然捕捉到了内存泄漏。
难道mFavoriteList = null这句话没有生效还是什么原因?
有关手动设置null这个问题能否触发GC回收在查阅了周志明老师《深入理解jvm虚拟机》一书,得到答案:这样做并不能触发GC回收。以下是书中摘录:
赋null值的操作在某些情况下确实是有用的,但笔者的观点是不应当对赋null值的操作有过多的依赖,更没有必要把它当做一个普 遍的编码规则来推广。原因有两点,从编码角度讲,以恰当的变量作用域来控制变量回收时间才是最优雅的解决方法,如代码清单8-3那样的场景并不多见。更关键的是,从执行角度讲,使用赋null值的操作来优化内存回收是建立在对字节码执行引擎概念模型的理解之上 的,在第6章介绍完字节码后,笔者专门增加了一个6.5节“公有设计、私有实现”来强调概念模型与实际执行过程是外部看起来等效,内部看上去则可以完全不同。在虚拟机使用解释器执行时,通常与概念模型还比较接近,但经过JIT编译器后,才是虚拟机执行代码的主要方 式,赋null值的操作在经过JIT编译优化后就会被消除掉,这时候将变量设置为null就是没有意义的。字节码被编译为本地代码后,对GC Roots的枚举也与解释执行时期有巨大差别,以前面例子来看,代码清单8-2在经过JIT编译后,System.gc()执行时就可以正确地回收掉内存,无须写成代码清单8-3的样子。
即:手动置null并不能保证GC一定会将该对象回收。那一个对象在什么时候才会被回收呢?依然引用书中的原话:
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃 脱,那基本上它就真的被回收了。
可以非常简单地理解(不一定正确):一个对象在为null且没有任何对象持有它的引用的时候,下一个回收周期到来时GC会回收。

2、延长对象的生命周期

既然手动置对象为null不能解决问题,那么延长对象的生命周期呢?尝试以下改动:

public FavoriteList(Context context, ListView list, FavoriteDatabaseListener listener,
            FileIconHelper iconHelper,Handler handler) {
        mContext = context;
        mFavoriteDatabase = new FavoriteDatabaseHelper(context.getApplicationContext(), this);
        mFavoriteDatabase.setListener(listener);
        ……
    }

将mFavoriteDatabase的生命周期设置和application的生命周期一致。这样改的出发点是既然单例模式FavoriteDatabaseHelper的实例mFavoriteDatabase持有了一个比它生命周期还短的对象MainActivity的引用,导致GC无法回收,那我把它的生命周期延迟,和application保持一致,理论上应该不会再有内存泄露了吧。改好之后编译运行,leakcanary依然报了内存泄露。这次报泄露的地方在mFavoriteDatabase.setListener(listener)这句,还是说单例模式FavoriteDatabaseHelper的实例mFavoriteDatabase持有了MainActivity的引用,导致内存泄露。分析代码发现,FavoriteList的构造函数有一个FavoriteDatabaseListener类型的形参listener,这个listener是MainActivity的匿名内部类。我们都知道匿名内部类会默认持有外部类的引用,结果就是在执行mFavoriteDatabase.setListener(listener)这句时,mFavoriteDatabase持有了MainActivity的引用。这个listener的作用是什么呢?就是一个回调接口,用于通知UI数据库变化。那这个泄露其实还是单例模式引起,仔细分析代码发现这个接口比较重要,无法通过其他方法修改。那怎么在保存这个接口的情况下不导致内存泄露呢?

3、使用弱引用WeakReference

弱引用WeakReference提供了若于强引用和软引用之后的引用关系,一个对象被Weakreference修饰而没有任何其他strong reference指向的时候, 如果GC运行, 那么这个对象就会被回收。修改之前的代码:

……
private FavoriteDatabaseListener mListener;

public FavoriteDatabaseHelper(Context context, FavoriteDatabaseListener listener) {
    super(context, DATABASE_NAME, null, DATABASE_VERSION);
    mListener = listener;
}
……

修改之后的代码:

……
private WeakReference<FavoriteDatabaseListener> mListener;

public FavoriteDatabaseHelper(Context context, FavoriteDatabaseListener listener) {
	super(context, DATABASE_NAME, null, DATABASE_VERSION);
 	mListener = new WeakReference<>(listener);
}
……

使用mListener之前需先判空:

mListener.get().

编译运行,这次不在有内存泄露,问题解决。
使用以下代码查看mFavoriteDatabase的引用情况,发现mListener不再持有MainActivity的引用:

Field[] dataBaseHelperField = mFavoriteDatabase.getClass().getDeclaredFields();

相关参考:https://medium.com/freenet-engineering/memory-leaks-in-android-identify-treat-and-avoid-d0b1233acc8
相关参考:https://stackoverflow.com/questions/34621640/when-an-anonymous-class-with-no-references-to-its-enclosing-class-is-returned-fr
相关参考:https://www.nowcoder.com/questionTerminal/fbef4d5971ce4009aa720aecf7d83f3c?pos=81&mutiTagIds=570&orderByHotValue=1
相关参考:https://blog.csdn.net/cao_dayong/article/details/64447191?locationNum=14&fps=1
相关参考:https://blog.csdn.net/xwx617/article/details/81193102

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值