美文网首页
记一次LeakCanary分析内存泄漏及处理

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

作者: 俗人浮生 | 来源:发表于2019-04-22 22:17 被阅读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

    相关文章

      网友评论

          本文标题:记一次LeakCanary分析内存泄漏及处理

          本文链接:https://www.haomeiwen.com/subject/vopowqtx.html