单例模式的内存泄漏陷阱
(本篇博客举了一个反面的例子,目的在于让新手如何去发现自己的错误)
最近项目开发中使用了一个叫做leakcanary的内存泄漏检查工具,当开发中的调试运行时发生内存泄漏,leakcanary会在notification弹出一个内存泄漏报告,最近发生了个内存泄漏并且leakcanary给出了下列报告:
分析下Leakcanary给出的信息,最后一行它说PopOrderActivity这个实例发生了泄漏,即系统gc的时候没有把这个activity给回收(本该回收的,应该是已经退出这个activity了),倒数第二行即说明了有一个叫做PendingOrderManager的类含有这个activity的引用,查看代码,这个PendingOrderManager是个单例,同时它的构造函数传入了一个Context参数:
public class PendingOrderManager { private static PendingOrderManager instance; private Context mContext; public PendingOrderManager(Context context) { this.mContext = context; } public static PendingOrderManager getInstance(Context context) { if (instance == null) { instance = new PendingOrderManager(context); } return instance; } ... }
之所以要传入个context是因为这个Manager里面需要创建Preference。
那么现在发生内存泄漏的原因也就很明了了,由于PendingOrderManager是一个单例模式,那么这个类的生命周期就伴随整个应用的生命周期,而它在被PopOrderActivity创建的时候引用了PopOrderActivity,所以当系统GC的时候试图去回收PopOrderActivity时,发现它却在被另一个任然在内存里的PendingOrderManager所引用,所以GC回收它失败,从而导致了内存泄漏。
那么如何解决这个问题呢?答案很简单,在PendingOrderManager中对context的属性使用弱引用即可:
public class PendingOrderManager { private static PendingOrderManager instance; private WeakReference<Context> wr; public PendingOrderManager(Context context) { L.d("PendingOrderManager <constructor>"); wr = new WeakReference<>(context); } public static PendingOrderManager getInstance(Context context) { if (instance == null) { instance = new PendingOrderManager(context); } return instance; } ... }
在PendingOrderManager中原来需要使用Context的地方,用wr.get()即可:
String timesListStr = (String) SPUtils.getPendingOrder(wr.get(), KEY_TIMES_LIST, ""); //这里的wr.get()原来是mContext
这里需要注意的一点是,由于PendingOrderManager这个时候含有的“context”可以被回收置空了,那么后面使用context的地方要注意判断是否为空,即对wr.get的地方注意检查空情况。
还有一种方式可以解决这个问题,考虑到每个使用到PendingOrderManager的地方当都会通过这种方式:
(PendingOrderManager.getInstance(mContext).getXXX()
即每次都能传过来一个当前的调用者的context(肯定不为空),那么在PendingOrderManager的getInstance方法里面除了判定instance是否为空外,最好在判定下wr.get是否为空,这样子若上一个实例化PendingOrderManager的activity被回收后,可以考虑用新的context来重新创建PendingOrderManager的单例。改造后的getInstance方法:
public class PendingOrderManager { private static PendingOrderManager instance; private WeakReference<Context> wr; public PendingOrderManager(Context context) { L.d("PendingOrderManager <constructor>"); wr = new WeakReference<>(context); } public static PendingOrderManager getInstance(Context context) { if (instance == null || wr.get() == null) { instance = new PendingOrderManager(context); } return instance; } ... }
关于ApplicationContext
既然这个单例Manager是需要被全局访问的,同时Manager里面又需要context,那么最好的方式就是用一个生命周期是整个app的context来代替。所以这个单例Manager并不需要构造的时候传入一个context,只需要在Manager里面使用context的地方通过getApplicationContext即可。因为application context的生命周期是最长的。
PS:
leakcanary是个很好的工具,下列是一些参考资料:
http://www.liaohuqiu.net/cn/posts/leak-canary-read-me/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?