介绍
内存泄露是平常开发中经常遇到的,有些时候稍不注意就会发生,而且还不易察觉,这就需要工具来帮助检测。本文主要介绍内存检测工具和我在开发中遇到的内存泄露问题和解决方案。
内存泄露的原理
具体的原理涉及到虚拟机垃圾回收机制知识,这里只为下文作介绍说明基本原理。想深度了解的请google。
内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。
内存检测工具
目前开发流行的检测主要分两种。
- 功能强大PC端检测工具,如
MemoryAnalyzer
运行在PC端抓取Android手机中的dump文件进行深度分析。 - 小而优的Android端检测工具,如
LeakCanary
随App一起安装会在Android手机桌面安装的内存泄露检测App
我目前使用的是LeakCanary分析内存泄露,好处是LeakCanary会一直检测debug生成的App,在App运行期间一直分析内存泄露情况,并抓取内存泄露关键代码,随时发现问题。
提供中文说明地址,使用比较简单就不说明,不了解的点进去看看。
容易引起内存泄漏的几大原因
- 静态集合类
像HashMap、Vector等静态集合类的使用最容易引起内存泄漏,因为这些静态变量的生命周期与应用程序一致。 - 监听器
在java编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。 - 物理连接
一些物理连接,比如数据库连接和网络连接,除非其显式的关闭了连接,否则是不会自动被GC 回收的。Java数据库连接一般用DataSource.getConnection()来创建,当不再使用时必须用Close()方法来释放,因为这些连接是独立于JVM的。对于Resultset和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。 - 内部类和外部模块等的引用 内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。
具体内存泄露问题
是本文的主要内容,作为记录我在开发中遇到的内存泄露问题和提出解决方案并分析内存泄露发生的原理,提供给大家参考。
okhttp拦截器导致的内存泄露
我的一个项目中使用Retrofit作为联网框架,其中一个需求是用文件下载,当然文件下载网上已经有很多开源框架,为了提升自己最好还是自己写。
具体的过程网络上很多。你只需要知道Retrofit是okhttp的包装框架核心功能都是在okhttp完成的,所以最后就是给OkHttp添加拦截器,监听网络响应体回调。
部分代码说明:
这是给okhttpBuilder构造器添加拦截器的代码,关键在于从外部传入的接口listener。
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();
httpBuilder.addNetworkInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder().body(new ProgressResponseBody(originalResponse.body(),listener)).build();
}
});
接口的实例化写在Activity的onCreate方法中,这样就能得到下载进度的回调,当然回调是发生在网络子线程中而不是UI线程。然后在相应的地方做点击事件开启网络下载事件。
public class ImageDetailActivity extends BaseActivity{
@Override
protected void onCreate(Bundle savedInstanceState) {
onProgressResponseListener=new OnProgressResponseListener() {
@Override
public void onResponseProgress(long bytesRead, long contentLength, boolean done) {
Logger.d("bytesRead="+bytesRead+" contentLength="+contentLength+" done"+done);
}
};
}
}
重点
重点来了,然后debug运行App跑着跑着就发生了内存泄露。Leaks分析完成后定位发生内存泄露的代码行。
、
分析:
- 定位到的代码行有点多,可以肯定的是一定是在添加接口实例化代码后产生的问题。
- 其实刚才就说到回调是发生在网络子线程,接口的实例化之后通过工具类被加入了OkHttpClient.Builder对象的内部变量
final List<Interceptor> networkInterceptors
中, - 然后这个变量通过生成器模式构建出了OkHttpClient对象,最后Activity中实例化的onProgressResponseListener对象和OkHttpClient对象形成了引用关系,而他们的属于不同线程生命周期中。
- 当用户按下返回键Activity的实例被finish()之后,OkHttpClient对象还存在,导致Activity实例不能被回收,从而产生了内存泄露。
总结
产生泄露的原因是两者发生引用,而生命周期不同。
上面的代码只是为了说明内存泄露发生的情况,当时在Activity中实例化onProgressResponseListener接口是为测试拦截器是否添加成功,是测试代码。真正的项目使用是不能这么写的,完整的写法网络上有很多,比如解决Retrofit文件下载进度显示问题。其中就是定义ProgressHelper类把ProgressListener接口实例化和OkHttp的构造写在同一个类里面,然后通过Handler发送消息实现UI层的回调。
单例引起的内存泄露
单例使用不当也会发生内存泄露,比如下面的代码,饿汉单例模式,当调用setup(Context context)
方法传入某个Activity实例的,当Activity被销毁,系统试图回收垃圾时,就会产生内存泄露。
public class AppSettings {
private Context mAppContext;
private static AppSettings sInstance = new AppSettings();
//some other codes
public static AppSettings getInstance() {
return sInstance;
}
public final void setup(Context context) {
mAppContext = context;
}
}
分析
原因是,这个Activity实例和单例产生了引用关系,而sInstance作为静态对象,其生命周期要长于普通的对象,其中就包括Activity实例。
解决
解决的方法就是不持有Activity的引用,而是持有Application的Context引用。因为Application的生命周期是贯穿整个App的运行期间的,这样两者的生命周期一致,就不会发生内存泄露。修改代码如下
public final void setup(Context context) {
mAppContext = context.getApplicationContext();
}
有关内存泄露的总结
- 不要让生命周期长于Activity的对象持有到Activity的引用
- 尽量使用Application的Context而不是Activity的Context
- 尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用
- 如果使用静态内部类,将外部实例引用作为弱引用持有。
总结
- 本文先介绍相关知识背景和内存检测工具
- 介绍容易引起内存泄漏的几大原因,并举例说明。
- 这篇博客作为内存泄露问题的总结,暂时就想到这么多,以后遇到问题解决之后会更新博文。