搞android的都知道有一个非常牛逼的工具:LeakCanary,用来检测内存泄露的,Github地址
首先,我们来说一下依赖注入的方式:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'
本来这个没什么好说的,但这里涉及到两个dependencies的注入类型:debugImplementation和releaseImplementation
我们都知道LeakCanary是用来检测内存泄露的,当检测到内存泄露时,会以弹窗的形式出现,一般也就是在debug时展现给开发者看就行了,上线的生产环境可千万不能弹,不然就尴尬了。
既然如此,按理来说在Application中的初始化代码(如下)应该进行BuildConfig.DEBUG的判断才对,但其实并不需要,这就是LeakCanary高明之处,它结合了上面两个dependencies的注入类型来实现的。
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
我们先来看看这两个dependencies的注入类型:
debugImplementation:只在buildType为debug的时候参与打包,release不参与打包
releaseImplementation:与debugImplementation正好相反,只在release模式下参与打包
这会,某些同学就会问了,既然LeakCanary只用于debug,那么就用debugImplementation来注入就行了,为什么还要用releaseImplementation呢?
这样在debug时完全没问题,但是,嘿嘿,你试试打release包一下就知道了,O(∩_∩)O~
直接打包不了的好不!如上所说的,debugImplementation不参与release打包,那么你在Application中的初始化代码会报找不到相应的类,这很好理解!
当然,如果你不怕麻烦,每次打release包都手动去把Application中的初始化代码注释掉,这样也是OK的!
但是,个人觉得LeakCanary的处理方法更加高明,直接区分debug和release两种依赖包,release包非常小,大概只有十几二十K的样子,只为确保打release包不报错就行了,这样就完美地解决了这个问题!
好啦,介绍完LeakCanary,下面我们直接正题,记录一次内存泄露的分析和处理
先介绍一下本次出现的内存泄露——Volley
PS:没办法,公司老项目就是用这个,那天有空就接入了LeakCanary测测看,结果傻眼了!
我们来先看看项目中涉及到的一些封装方法
public class MyVolleyTool {
//创建一个静态的请求队列
public static RequestQueue mRequestQueue = Volley.newRequestQueue(Global.getContext());
//定义一个接口,用于处理请求成功和失败
public interface IResponse {
void subscribeData(MyResponse data);
void subscribeError();
}
/**
* Post方式从网络获取数据
*/
public static void postDataFromNet(final IResponse iResponse, final String url, final Map map) {
JsonObjectRequest request=new JsonObjectRequest(
Request.Method.POST, url, new JSONObject(map),
new Response.Listener() {
@Override
public void onResponse(JSONObject response) {
iResponse.subscribeData(new MyResponse(response));//将数据返回
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError volleyError) {
iResponse.subscribeError();
}
}
);
//设置超时重新请求
request.setRetryPolicy(new DefaultRetryPolicy(20 * 1000, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
request.setTag("" + url);
mRequestQueue.add(request);
}
}
大概也就是这样子,非常简单的封装,然后我们再来看看在Activity中的调用:
MyVolleyTool.postDataFromNet(new MyVolleyTool.IResponse(){
@Override
public void subscribeData(MyResponse data) {
······
}
@Override
public void subscribeError() {
······
}
},HttpUrl.URL_XXX,map);
调用起来就更加简单了,这样平时用起来感觉都没问题,而且随着现在手机内存大,虚拟机垃圾回收的各种优化,就算了用了很久都没发现什么问题,然而你用LeakCanary进行测试,结果就是只要某个activity中有用上面Volley代码进行网络请求的,退出该activity后总会报内存泄露,如下:
都说LeakCanary是神器,界面已经很清楚地说明了问题,其实这次的内存泄露也是老生常谈的问题:匿名内部类持有外部类的引用,外部类不能被垃圾回收器回收,所以导致内存泄漏。
回到我们上面这个例子来,很显然我们在activity中创建了这么一个对象:MyVolleyTool.IResponse,所以它就持有当前activity的引用,再然后我们创建了两个Listener,用于处理网络请求返回的结果,这里需要说明一下,由于网络操作和UI操作是异步的,而上面我们创建的Response.Listener和Response.ErrorListener中的UI操作都使用到了iResponse,换句话说,两个Listener都持有iResponse对象,故而间接地也持有了当前activity的引用。
我们也可以做个实验来验证上面的分析是否正确,比如,你将上面Response.Listener中处理正常返回网络数据onResponse方法中的iResponse那一行代码注释掉,如下:
//iResponse.subscribeData(new MyResponse(response));//将数据返回
如上图,我们注意到内存泄露的对象由原来的mListener变为mErrorListener,现在情况已经很明了啦,当然,你也可以试试把两个Listener中使用到的iResponse对象都注释掉,那么,内存泄露就不复存在了!
当然,上面只是为了做实验验证,现实情况是,我们必须使用iResponse来将网络请求的结果返回,这一点请明确!
好啦,原因我们已经分析清楚了,下面我们看看如何解决这个问题的,有两种方案:
1、使用弱引用
我们都知道Java有四大引用类型:强>软>弱>虚
今天我们就用弱引用来处理内存泄露,其实有了上面的分析,那么我们可以很容易地想到方案,那就是:两个Listener持有iResponse的弱引用,OK,我们直接上代码:
public class WeakResponseListener implements Response.Listener, Response.ErrorListener{
private WeakReference weakIResponse;
public WeakResponseListener(MyVolleyTool.IResponse iResponse) {
weakIResponse = new WeakReference<>(iResponse);
}
@Override
public void onResponse(JSONObject response) {
MyVolleyTool.IResponse iResponse = weakIResponse.get();
if(iResponse!=null){
iResponse.subscribeData(new MyResponse(response));//将数据返回
}
}
@Override
public void onErrorResponse(VolleyError error) {
MyVolleyTool.IResponse iResponse = weakIResponse.get();
if(iResponse!=null) {
iResponse.subscribeError();
}
}
}
代码非常简单,创建了一个类同时实现Response.Listener和Response.ErrorListener,然后我们在onResponse和onErrorResponse中使用的都是iResponse的弱引用,这样就解决问题了,然后,我们的封装方法直接变为:
/**
* Post方式从网络获取数据
*/
public static void postDataFromNet(final IResponse iResponse, final String url, final Map map) {
JsonObjectRequest request=new JsonObjectRequest(
Request.Method.POST, url, new JSONObject(map),
new WeakResponseListener(iResponse),
new WeakResponseListener(iResponse)
);
//设置超时重新请求
request.setRetryPolicy(new DefaultRetryPolicy(20 * 1000, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
request.setTag("" + url);
mRequestQueue.add(request);
}
这个就没什么好说的了,说白了,就是通过使用弱引用将两个Listener在activity销毁后能够正常被GC回收,就是这么简单!
2、使用反射
既然LeakCanary已经很明确的告诉我们内存泄露的对象,那么,我们只需在网络请求之后,将相应的对象置空即可达到目的,这个就更加霸道直接了,当然,因为我们使用的是Volley第三方框架,所以,并不是所有的对象都有相应的API可以获取到的,于是,反射大法好啊,有了反射还怕拿不到对象吗?
我们来看看如何处理上面的问题,上面LeakCanary已经明明白白告诉我们泄露的对象是:mListener和mErrorListener,那么,我们只需将这两个对象所有的引用都设置为null就可以了,直接上代码:
public class MyJsonObjectRequest extends JsonObjectRequest {
private Response.Listener listener;
private Response.ErrorListener errorListener;
public MyJsonObjectRequest(int method, String url, JSONObject jsonRequest, Response.Listener listener, Response.ErrorListener errorListener) {
super(method, url, jsonRequest, listener, errorListener);
this.listener=listener;
this.errorListener=errorListener;
}
@Override
protected void deliverResponse(JSONObject response) {
super.deliverResponse(response);
clearListener();//清空Listener
}
@Override
public void deliverError(VolleyError error) {
super.deliverError(error);
clearListener();//清空Listener
}
//清空Listener
private void clearListener(){
listener=null;
errorListener=null;
try {
Field field1 = JsonRequest.class.getDeclaredField("mListener");
field1.setAccessible(true); // 参数值为true,禁止访问控制检查
field1.set(this,null);
Field field2 = Request.class.getDeclaredField("mErrorListener");
field2.setAccessible(true); // 参数值为true,禁止访问控制检查
field2.set(this,null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
这次我们扩展的是Request对象,在网络请求结束的回调方法:deliverResponse和deliverError中,我们对两个Listener对象都进行处理,将其设置为null,就如同张三丰太极拳所说的——“让其根自断”。
然后我们只需用MyJsonObjectRequest去替换原来的JsonObjectRequest即可,这里就不再贴代码了。
测试验证
虽然,上面两种方案都能让LeakCanary不再报内存泄露的错误,但是,我们还想通过内存对象来进一步分析确认问题,这里就直接使用AndroidStudio的Profiler工具面板进行测试:
如上图所示,我们不停地进出同一个带Volley网络请求的activity,可以看到内存一直上升,但当达到一定程度时,倒也可以释放掉,估计这也就是为什么明明Volley内存泄露了但不至于内存溢出的原因吧!
接下来,我们直接对堆内存做对比分析,如下图:
多次进出同一activity,内存泄露的话在堆内存中可以找到多个activity实例(如图为4个实例存活,看的是“Total Count”),而内存泄露解决后堆内存只有唯一的一个activity实例,好啦,对比很明显,不用多说什么了!
至此,我们使用LeakCanary完成了一次内存泄露的处理,事实再次证明,请小心使用匿名内部类,否则内存泄漏将如影相随!
最后,再说一个题外的东东:StrictMode,我们可用它来帮助发现代码中的一些不规范的问题,详情可参考:StrictMode