记一次LeakCanary分析内存泄漏及处理

2019-04-22  本文已影响0人  俗人浮生

搞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<String, Object> map) {
        JsonObjectRequest request=new JsonObjectRequest(
                Request.Method.POST, url, new JSONObject(map),
                new Response.Listener<JSONObject>() {
                    @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后总会报内存泄露,如下:

Volley内存泄露

都说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<JSONObject>, Response.ErrorListener{

    private WeakReference<MyVolleyTool.IResponse> 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<String, Object> 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<JSONObject> listener;
    private Response.ErrorListener errorListener;

    public MyJsonObjectRequest(int method, String url, JSONObject jsonRequest, Response.Listener<JSONObject> 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

上一篇下一篇

猜你喜欢

热点阅读