说起网络通信框架,不得不说到Volley,虽然还有很多其它优秀的第三方框架,比如OkHttp,Retrofit,但Volley凭借着它的轻量化,Google官方出品,还是有一席之地的。但是,Volley也存在着一些坑,这篇博客主要聚焦Volley造成的内存泄漏问题。

分析

Request

通过Volley构建网络请求的时候,不管是StringRequest,还是JsonRequest,或者是ImageRequest,都要传入Response.ListenerResponse.ErrorListener的参数,以StringRequest为例:

1
2
3
4
5
6
7
8
9
10
11
12
StringRequest stringRequest = new StringRequest("http://example.com",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
}
});

在这里面处理网络请求的结果,也就是说,这两个Listener可能会包含一些回调,通知View层去更新数据,这个回调可能是Activity的引用,或者是Presenter的引用,或者是其他实例的引用。

那么我们再来看看,当一个Request被cancel之后,Volley内部执行了什么操作

1
2
3
4
5
6
/**
* Mark this request as canceled. No callback will be delivered.
*/
public void cancel() {
mCanceled = true;
}

这里只是更新了一下标识符

由于StringRequest是继承于Request的,所以猜想可能是在子类对Listener进行了置空处理。StringRequest源码并不多,索性全部贴出来以供研究。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class StringRequest extends Request<String> {
private final Listener<String> mListener;
public StringRequest(int method, String url, Listener<String> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
this(Method.GET, url, listener, errorListener);
}
@Override
protected void deliverResponse(String response) {
mListener.onResponse(response);
}
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
String parsed;
try {
parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
parsed = new String(response.data);
}
return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}
}

可以看到,在创建Request时的Listener到了构造函数时赋给了全局变量mListener,而ErrorListener则继续向父类,也就是Request<T>传递。其他种类的Request基本保持这一做法。

我们可以发现,不管是父类还是子类,都没有在cancel()方法里对mListener进行置空操作,这就会带来一个问题:当一个Request被取消时,由于mListener没有被置空,创建出来的Request实例依旧持有Listener的实例,当Listener实例是一个Activity的话,很有可能导致Activity退出后,由于Activity的引用被Request持有而导致内存不能被及时回收,造成泄漏。这种情况同样试用于ErrorListener

ImageLoader

由于ImageLoader内部也是通过ImageRequest来请求图片资源的,既然ImageRequest会出现内存泄露,ImageLoader也肯定会。这里附上ImageLoader请求图片资源的关键方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
...
//ImageLoader的图片缓存机制
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
//无法找到图片缓存,发起网络请求获取图片资源
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
imageListener.onResponse(imageContainer, true);
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
request.addContainer(imageContainer);
return imageContainer;
}
//通过ImageRequest获取图片
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
cacheKey);
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}

解决

  • Request
    思路很简单,对于每一个Request,比如StringRequestJsonRequestImageRequest等等,重新写一个类,直接继承于各自功能的XXRequest<T>,重写cancel()方法,在里面对Listener进行置空操作,同时,需要将ListenerErrorListener的所有回调操作全部托付给子类,保证XXRequest<T>中不含有ListenerErrorListener。这里还是以StringRequest举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public abstract class LeakFreeStringRequest<T> extends StringRequest<T> {
    protected Map<String, String> mParamMap;
    private Response.Listener<T> mListener;
    private Response.ErrorListener mErrorListener;
    public LeakFreeStringRequest(int method, String url, Map<String, String> params, Listener<T> listener, ErrorListener errorListener) {
    super(method, url, null);
    mListener = listener;
    mErrorListener = errorListener;
    mParamMap = params;
    }
    @Override
    public void cancel() {
    mListener = null;
    mErrorListener = null;
    super.cancel();
    }
    @Override
    protected void deliverResponse(T response) {
    mListener.onResponse(response);
    }
    @Override
    public void deliverError(VolleyError error) {
    mErrorListener.deliverError(error);
    }
    @Override
    public ErrorListener getErrorListener() {
    return mErrorListener;
    }
    ...

所有其他特定功能的Request类都可以参照这样来实现。当然,如果你是源码编译引用的Volley,直接改源码就好了

  • ImageLoader
    虽然留了这个坑,好在ImageLoadermakeImageRequest()方法可以让我们自定义构建请求的操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
    ScaleType scaleType, final String cacheKey) {
    return new ImageRequest(requestUrl, new Listener<Bitmap>() {
    @Override
    public void onResponse(Bitmap response) {
    onGetImageSuccess(cacheKey, response);
    }
    }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
    onGetImageError(cacheKey, error);
    }
    });
    }

源码中的makeImageRequest()中创建的ImageRequest是直接继承于Request<T>的,根据上文分析的结论必须替换成改造版本的,这里姑且叫做LeakFreeImageRequest,代码就不贴了,和上文LeakFreeStringRequest类写法类似。接着,需要新建一个类,暂且叫LeakFreeImageLoader,重写makeImageRequest()方法,将ImageRequest改为已经改造好的LeakFreeImageRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LeakFreeImageLoader extends ImageLoader {
public LeakFreeImageLoader(RequestQueue queue, ImageCache imageCache) {
super(queue, imageCache);
}
@Override
protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
ImageView.ScaleType scaleType, final String cacheKey) {
return new LeakFreeImageRequest(requestUrl, new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
onGetImageSuccess(cacheKey, response);
}
}, maxWidth, maxHeight, scaleType, Bitmap.Config.RGB_565, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onGetImageError(cacheKey, error);
}
});
}
}

之后,替换原有的ImageLoader变为LeakFreeImageLoader,问题解决