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

时间:2016-09-22 05:11:13

标签: android web-services memory-leaks android-volley leakcanary

我正在使用排球库进行网络服务调用。我创建了一个通用类,用于调用所有Web服务并从那里进行服务调用,并使匿名监听器成功并响应错误。

但是当我使用泄漏金丝雀时,它显示与上下文相关的内存泄漏。以下是我的代码片段:

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));
    }
}

我创建了一个名为response listener的接口,用于重定向对activity或fragment的响应。我提出如下要求。

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

任何想要删除此泄漏的任何帮助都表示赞赏。

4 个答案:

答案 0 :(得分:5)

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

stringRequest.setTag(activityInstance);

取消所有待处理的请求:

requestQueue.cancellAll(activityInstance);

此外,在VolleySingleton中使用Application上下文来创建RequestQueue。

mRequestQueue = Volley.newRequestQueue(applicationContext);

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

答案 1 :(得分:1)

基本上,匿名方法在Android或任何ClientSideSystem中都很糟糕,因为你没有大量记忆。发生的事情是,您已将Context作为参数传递给方法,anonymous拥有该参考。真正的混乱现在出现在场景中,因为network call内部的线程无法完成它的工作,在此之前由于某种原因,调用活动要么在这种情况下破坏或回收GC,无法收集活动,因为wokerThread可能仍然在引用它。有关详细说明,请浏览this

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

WeakReference的优点是,如果没有其他人,如果对其进行引用,它将允许GC收集该对象。

答案 2 :(得分: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();

这解决了泄漏问题。

答案 3 :(得分:0)

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

问题在于最后一个请求对象在Network Dispatcher&amp;缓存调度程序。

@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,因为它已被弃用,从那时起就有这个bug。为了使用Volley,请选择:

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

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