凌空请求的匿名侦听器导致内存泄漏

Posted

技术标签:

【中文标题】凌空请求的匿名侦听器导致内存泄漏【英文标题】:Anonymous Listener of volley request causing memory leak 【发布时间】:2017-01-30 12:57:19 【问题描述】:

我正在使用 volley 库进行网络服务调用。我制作了一个通用类,用于调用所有 Web 服务并从那里进行服务调用,并为成功和错误响应设置了匿名侦听器。

但是当我使用泄漏金丝雀时,它会显示与上下文相关的内存泄漏。下面是我的sn-p代码:

public void sendRequest(final int url, final Context context, final ResponseListener responseListener, final Map<String, String> params) 
    StringRequest stringRequest;
    if (isNetworkAvailable(context)) 

      stringRequest = new StringRequest(methodType, actualURL + appendUrl, new Listener<String>() 
            @Override
            public void onResponse(String response) 
                dismissProgressDialog(context);
                try 
                    (responseListener).onResponse(url, response);
                 catch (JsonSyntaxException e) 
                    // Util.showToast(context, context.getResources().getString(R.string.error));
                    Crashlytics.logException(e);
                
            

        , new ErrorListener() 
            @Override
            public void onErrorResponse(VolleyError error) 
                // Util.showToast(context,context.getString(R.string.error));

                dismissProgressDialog(context);
                if (error instanceof NetworkError) 
                     Util.showToast(context, context.getResources().getString(R.string.network_error));
                 else if (error instanceof NoConnectionError) 
                     Util.showToast(context, context.getResources().getString(R.string.server_error));
                 else if (error instanceof TimeoutError) 
                     Util.showToast(context, context.getResources().getString(R.string.timeout_error));
                 else 
                     Util.showToast(context, context.getResources().getString(R.string.default_error));
                


            

        ) 
            @Override
            protected Map<String, String> getParams() throws AuthFailureError 
                return params;
            


            @Override
            public Map<String, String> getHeaders() throws AuthFailureError 
                return request.getHeaders(context, actualURL, false);
            
        ;
        stringRequest.setRetryPolicy(new DefaultRetryPolicy(30000, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
        VolleySingleton.getInstance(context).addRequest(stringRequest);
     else 
         Util.showToast(context, context.getString(R.string.internet_error_message));
    

我创建了一个名为响应侦听器的接口,用于将响应重定向到活动或片段。我提出如下要求。

Request.getRequest().sendRequest(Request.SOME URL, SplashScreenActivity.this, SplashScreenActivity.this, new HashMap<String, String>());

但我正面临内存泄漏:

In 2.1.1:31.
* activity.SplashScreenActivity has leaked:
* GC ROOT com.android.volley.NetworkDispatcher.<Java Local>
* references network.Request$5.mListener (anonymous subclass of com.android.volley.toolbox.StringRequest)
* references network.Request$3.val$responseListener (anonymous implementation of com.android.volley.Response$Listener)
* leaks activity.SplashScreenActivity instance
* Retaining: 1.2MB.
* Reference Key: b8e318ea-448c-454d-9698-6f2d1afede1e
* Device: samsung samsung SM-G355H kanas3gxx
* Android Version: 4.4.2 API: 19 LeakCanary: 1.4 6b04880
* Durations: watch=5052ms, gc=449ms, heap dump=2617ms, analysis=143058ms

感谢任何解决此泄漏的想法。

【问题讨论】:

【参考方案1】:

我在 LeakCanary 中检测到了类似的问题,其中 Volley 的 mListener 引用了我的响应侦听器,而我的侦听器引用了 ImageView,因此它可以使用下载的图像对其进行更新。

我将响应侦听器设置为活动中的内部类..

private class MyVolleyResponseListener <T> implements com.android.volley.Response.Listener <Bitmap> 

        @Override
        public void onResponse(Bitmap bitmap) 
            thumbNailView.setImageBitmap(bitmap);
        

.. 并在活动中的 onDestroy() 中停止并启动凌空请求队列..

requestQueue.stop();
requestQueue.start();

这已经修复了泄漏。

【讨论】:

【参考方案2】:

我知道我加入聚会有点晚了,但几天前这个问题确实破坏了我的周末。为了弄清楚,我继续研究了一下,最终得到了解决方案。

问题在于最后一个请求对象在 Network Dispatcher & Cache Dispatcher 中泄露。

@Override
    public void run() 
        if (DEBUG) VolleyLog.v("start new dispatcher");
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        // Make a blocking call to initialize the cache.
        mCache.initialize();

        Request<?> request;
        while (true) 
            // release previous request object to avoid leaking request object when mQueue is drained.
            request = null;
            try 
                // Take a request from the queue.
                request = mCacheQueue.take();
             catch (InterruptedException e) 
                // We may have been interrupted because it was time to quit.
                if (mQuit) 
                    return;
                
                continue;
            
            try 
                request.addMarker("cache-queue-take");

                // If the request has been canceled, don't bother dispatching it.
                if (request.isCanceled()) 
                    request.finish("cache-discard-canceled");
                    continue;
                

                // Attempt to retrieve this item from cache.
                Cache.Entry entry = mCache.get(request.getCacheKey());
                if (entry == null) 
                    request.addMarker("cache-miss");
                    // Cache miss; send off to the network dispatcher.
                    mNetworkQueue.put(request);
                    continue;
                

                // If it is completely expired, just send it to the network.
                if (entry.isExpired()) 
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                    mNetworkQueue.put(request);
                    continue;
                

                // We have a cache hit; parse its data for delivery back to the request.
                request.addMarker("cache-hit");
                Response<?> response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) 
                    // Completely unexpired cache hit. Just deliver the response.
                    mDelivery.postResponse(request, response);
                 else 
                    // Soft-expired cache hit. We can deliver the cached response,
                    // but we need to also send the request to the network for
                    // refreshing.
                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);

                    // Mark the response as intermediate.
                    response.intermediate = true;

                    // Post the intermediate response back to the user and have
                    // the delivery then forward the request along to the network.
                    final Request<?> finalRequest = request;
                    mDelivery.postResponse(request, response, new Runnable() 
                        @Override
                        public void run() 
                            try 
                                mNetworkQueue.put(finalRequest);
                             catch (InterruptedException e) 
                                // Not much we can do about this.
                            
                        
                    );
                
             catch (Exception e) 
                VolleyLog.e(e, "Unhandled exception %s", e.toString());
            
        

如您所见,一个新的请求对象是在从队列中取出之前创建的。这克服了内存泄漏的问题。

P.S:不要使用 Google 存储库中的 Volley,因为它已被弃用并且从那时起就有这个错误。为了使用 Volley,请这样做:

https://github.com/mcxiaoke/android-volley

上述存储库没有任何内存泄漏。再见。

【讨论】:

根据它的 README,上面的存储库也被弃用了,我还应该使用它吗? @droidster 这个实际上是带有泄漏网络调度程序的存储库【参考方案3】:

通常,匿名类对封闭类实例具有强引用。在您的情况下,这将是 SplashScreenActivity。现在我猜,您的Activity 在您通过 Volley 从服务器获得响应之前就完成了。由于侦听器对封闭的 Activity 具有强引用,因此在 Anonymous 类完成之前,该 Activity 不能被垃圾收集。你应该做的是用Activity实例标记你发送的所有请求,并在Activity的onDestroy()回调处取消所有请求。

stringRequest.setTag(activityInstance);

取消所有待处理的请求:

requestQueue.cancellAll(activityInstance);

另外,使用 VolleySingleton 中的应用程序上下文来创建 RequestQueue。

mRequestQueue = Volley.newRequestQueue(applicationContext);

不要在那里使用您的 Activity 上下文,也不要在 VolleySingleton 中缓存您的 Activity 实例。

【讨论】:

我遇到了这个问题,我尝试使用标签cancellAll()。但由于某种原因,内存泄漏仍在继续。然后我使用cancellAll() 并传递了一个取消all requests 的请求过滤器。问题消失了。我不知道为什么会这样! @Dinesh 这是正确的方法,但在活动中注入此代码将使此解决方案与该特定活动紧密结合,您需要在每个进行网络调用的活动中执行相同的逻辑 @NayanSrivastava 实际上,我不从 Activity 进行网络调用。我将拥有视图层、域层和数据层。只有数据层通过 Volley 发送请求。顺便说一句,setTag 接受任何对象 stringRequest.setTag(anyObject); @DineshBob 很好,我只是在争论,而不是在外部控制 API 调用,如果我们在进行 API 调用的层中处理它们会怎样【参考方案4】:

基本上,匿名方法在Android 或任何没有大量内存的ClientSideSystem 中都很糟糕。发生的事情是,您已将Context 作为方法中的参数传递,anonymous 持有它的引用。真正的混乱现在出现在场景中,其中使network call 内部的线程无法完成它的工作,并且在此之前调用活动由于某种原因在这种情况下销毁或回收GC 无法将活动收集为@ 987654328@ 可能仍在对它进行引用。详细说明请通过this。

解决方案可以是静态内部类或独立类,在这两种情况下都使用WeakReference 来保存资源并在使用它们之前进行空检查。

WeakReference 的优点是它允许GC 在没有其他人的情况下收集对象,如果持有对它的引用。

【讨论】:

@Nayan Srivastava 你能编辑代码吗,我不知道如何在现有代码中解决这个问题 而不是new Listener&lt;String&gt;() @Override public void onResponse(String response) dismissProgressDialog(context); try (responseListener).onResponse(url, response); catch (JsonSyntaxException e) // Util.showToast(context, context.getResources().getString(R.string.error)); Crashlytics.logException(e); 直接创建一个实现这个的类并在里面传递那个类对象 我必须创建实现 Listner 接口的类,并且必须在那里传递该类对象。像 class ResponseClass 实现 Listener,ErrorListener Override public void onResponse(Object response) Override public void onErrorResponse(VolleyError error) @Nayan Srivastava 我无法清楚地理解我是否必须从我发出请求的点或在此方法中传递该类对象

以上是关于凌空请求的匿名侦听器导致内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

在 Handler 线程的队列中添加匿名可运行对象会导致内存泄漏吗?

在静态方法中使用匿名 Lamba 订阅事件会导致内存泄漏吗?

事件侦听器中的内存泄漏

如何在 OnDestroy 中删除此 firebase 侦听器以减少内存泄漏?

内存泄漏篇--1

发布 MainThread Runnables 内存泄漏安全 Android