Volley 源码解析及对 Volley 的扩展(一)

由于项目开始的时间比较早,其中的网络请求使用的还是 Volley 网络请求库,不过也不是直接使用 Volley,而是对 Volley 进行了一个简单的封装。前段时间提了一个新的需求,就是需要对每次网络请求的耗时和请求结果进行统计,并上传服务器。 针对此功能需求,就引出了本系列的文章。

针对本系列博客,写了一个简单的练习 Volley 的 VolleyPractice,放在了 GitHub上,欢迎沟通交流。

本文是Volley 源码解析及对 Volley 的扩展系列的第一篇文章,主要介绍以下内容:

  1. Volley 的简单使用及简单封装
  2. 简单却不准确的统计时长的方法
  3. 改进后更加准确的统计时长的方法

Volley 的简单使用及简单封装

Volley 的简单使用

Volley 使用还是比较简单方便的,相关的资料和博客有很多,而且不是本文在重点,简单放点代码,展示一个简单的 GET 请求,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String url = "http://gank.io/api/data/Android/10/1";
// 创建一个 Request 的对象,并设置好回调函数,StringRequest 默认是 GET 请求
StringRequest stringRequest = new StringRequest(url, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.i("lijk", "onResponse " + response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.i("lijk", "onErrorResponse " + error);
}
});
// 创建一个请求队列 RequestQueue 的对象
RequestQueue queue = Volley.newRequestQueue(MainActivity.this.getApplicationContext());
// 将请求加入到请求队列中,就会执行网络请求
queue.add(stringRequest);

Volley 的简单封装

先来看看封装后,进行一次和上面相同的网络请求的代码:

1
2
3
4
5
6
7
8
9
10
11
12
String url = "http://gank.io/api/data/Android/10/1";
VolleyManager.getInstance(MainActivity.this).addStringRequest(url, new OnHttpListener<String>() {
@Override
public void onSuccess(String result) {
Log.i("lijk", "onSuccess " + result);
}
@Override
public void onError(VolleyError error) {
Log.i("lijk", "onError " + error);
}
});

可以看到,两次请求从功能和效果来看是一样的,但是代码要简洁明了一些,下面对上面的代码做一些解释。

其中 OnHttpListener 是一个回调接口,用于回调请求的结果,由于有 成功失败 两种结果,所以有两个回调方法。如下代码所示:

1
2
3
4
5
6
public interface OnHttpListener<T> {
void onSuccess(T result);
void onError(VolleyError error);
}

VolleyManager 是一个管理 Volley 的类,具有如下特点:

  • 其中使用单例模式,保证在整个应用生命周期内只存在一个 VolleyManager 对象
  • 使用弱引用的方式持有 Context 对象,防止造成内存泄露
  • VolleyManager 中只有一个 RequestQueue 对象,以免创建多个 RequestQueue 对象,从而造成资源的浪费

VolleyManager 的代码如下所示:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
public class VolleyManager {
private static VolleyManager INSTANCE = null;
private static WeakReference<Context> mWRContext = null;
private RequestQueue mQueue = null;
private VolleyManager(Context context) {
if (mWRContext == null || mWRContext.get() == null) {
mWRContext = new WeakReference<>(context);
}
mQueue = getRequestQueue();
}
public static VolleyManager getInstance(Context context) {
if (INSTANCE == null) {
synchronized (VolleyManager.class) {
if (INSTANCE == null) {
INSTANCE = new VolleyManager(context);
}
}
}
return INSTANCE;
}
public void addStringRequest(String url, final OnHttpListener httpListener) {
this.addStringRequest(Request.Method.GET, url, httpListener);
}
public void addStringRequest(int method, final String url, final OnHttpListener<String> httpListener) {
StringRequest request = new StringRequest(method, url, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
httpListener.onSuccess(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
httpListener.onError(error);
}
});
addRequest(request);
}
public void addJsonObjectRequest(String url, JSONObject jsonRequest, final OnHttpListener<JSONObject> httpListener) {
this.addJsonObjectRequest(jsonRequest == null ? Request.Method.GET : Request.Method.POST, url, jsonRequest, httpListener);
}
public void addJsonObjectRequest(int method,final String url, JSONObject jsonRequest, final OnHttpListener<JSONObject> httpListener) {
JsonObjectRequest request = new JsonObjectRequest(method, url, jsonRequest, new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
httpListener.onSuccess(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
httpListener.onError(error);
}
});
addRequest(request);
}
public void addJsonArrayRequest(final String url, final OnHttpListener<JSONArray> httpListener) {
this.addJsonArrayRequest(Request.Method.GET, url, null, httpListener);
}
public void addJsonArrayRequest(int method, final String url, JSONArray jsonRequest, final OnHttpListener<JSONArray> httpListener) {
JsonArrayRequest request = new JsonArrayRequest(method, url, jsonRequest, new Response.Listener<JSONArray>() {
@Override
public void onResponse(JSONArray response) {
httpListener.onSuccess(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
httpListener.onError(error);
}
});
addRequest(request);
}
public void addImageRequest(String url, int maxWidth, int maxHeight,
Bitmap.Config decodeConfig, final OnHttpListener<Bitmap> httpListener) {
this.addImageRequest(url, maxWidth, maxHeight, ImageView.ScaleType.CENTER_INSIDE, decodeConfig, httpListener);
}
public void addImageRequest(final String url, int maxWidth, int maxHeight,
ImageView.ScaleType scaleType, Bitmap.Config decodeConfig,
final OnHttpListener<Bitmap> httpListener) {
ImageRequest request = new ImageRequest(url, new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
httpListener.onSuccess(response);
}
}, maxWidth, maxHeight, scaleType, decodeConfig, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
httpListener.onError(error);
}
});
addRequest(request);
}
private void addRequest(Request request) {
if (request == null) {
return;
}
mQueue.add(request);
}
private RequestQueue getRequestQueue() {
if (mQueue == null && mWRContext != null && mWRContext.get() != null) {
// getApplicationContext() is key, it keeps you from leaking the
// Activity or BroadcastReceiver if someone passes one in.
mQueue = Volley.newRequestQueue(mWRContext.get().getApplicationContext());
}
return mQueue;
}
}

整体来讲,代码还是不难的,要得懂还是不难的。

简单却不准确的统计方法

统计的回调接口

首先,需要明确一下统计的数据:网络请求的耗时、网络请求的结果(正确/失败)。每次网络请求都会对应一个 URL 的地址,所以需要把耗时、结果和 URL 对应起来。
根据上面的想法,创建一个回调接口,如下所示:

1
2
3
4
5
6
7
8
9
10
public interface OnResponseInfoInterceptor {
/**
* 网络请求耗时、网络请求结果的回调接口
*
* @param url 网络请求对应的 URL
* @param networkTimeMs 网络请求的耗时
* @param statusCode 网络请求结果的状态码(其实就是 Http 响应中的状态码)
*/
void onResponseInfo(String url, long networkTimeMs, int statusCode);
}

统计方法

如果对 OkHttp 有所了解的话,那应该知道在 OkHttp 有 拦截器 这个东西。拦截器顾名思义,它可以拦截一些东西,可以拦截发出的请求,也可以拦截收到的响应,从而可以对请求或者响应做一些处理(我是这么理解的,如果不同的想法和见解,欢迎沟通交流jiankunli24@gmail.com)。

因为需要统计网络请求结果的状态码,必须是在收到请求结果之后才可以得到;而网络请求耗时,则比较容易想得到,在网络请求开始的时候记录一下,在收到网络响应之后再次记录一下,中间这段时间就是网络请求的耗时了。

按照上面的思路,对 VolleyManager 中发送网络请求方法稍微进行修改,如下所示:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class VolleyManager {
private OnResponseInfoInterceptor mResponseInfoListener = null;
....
public void setOnResponseInfoInterceptor(OnResponseInfoInterceptor responseInfoListener) {
mResponseInfoInterceptor = responseInfoListener;
}
....
public void addStringRequest(String url, final OnHttpListener httpListener) {
this.addStringRequest(Request.Method.GET, url, httpListener);
}
public void addStringRequest(int method, final String url, final OnHttpListener<String> httpListener) {
// 记录网络请求开始的时间
final long startTimeStamp = SystemClock.elapsedRealtime();
StringRequest request = new CustomStringRequest(method, url, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
httpListener.onSuccess(response);
// 收到正确的网络请求结果
handleInterceptor(url, startTimeStamp, 1, interceptor);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
httpListener.onError(error);
// 收到错误的网络请求结果
handleInterceptor(url, startTimeStamp, 0, interceptor);
}
})
addRequest(request);
}
/**
* 处理 OnResponseInfoInterceptor 拦截器
*
* @param url 网络请求对应的URL
* @param startTimeStamp 网络请求开始的时间,用于计算网络请求耗时
* @param statusCode 网络请求结果的状态,1表示网络请求成功,0表示网络请求失败
* @param interceptor 拦截器 {@link OnResponseInfoInterceptor} ,有一个默认的拦截器 mResponseInfoInterceptor,
* 用户也可以通过{@link #addStringRequest(int, String,
* OnHttpListener, OnResponseInfoInterceptor)} 给网络请求设置单独的拦截器
*/
private void handleInterceptor(String url, long startTimeStamp, int statusCode,
OnResponseInfoInterceptor interceptor) {
long apiDuration = SystemClock.elapsedRealtime() - startTimeStamp;
if (apiDuration < 0 || TextUtils.isEmpty(url) || interceptor == null) {
return;
}
interceptor.onResponseInfo(url, apiDuration, statusCode);
}
....
}

在上面的代码中,我只举了做 String 请求的例子,其他 JsonObjectJsonArrayBitmap 请求的例子也是一样的。

在业务代码中像下面这样使用:

1
2
3
4
5
6
7
8
9
10
// 通过 {@link VolleyManager#setOnResponseInfoInterceptor(OnResponseInfoInterceptor)} 设置拦截器,这样应用中所有通过
// VolleyManager 发出的网络请求都会将网络请求的相关信息(url, networkTimeMs, statusCode) 回调到这儿。
VolleyManager.getInstance(MyApplication.this).setOnResponseInfoInterceptor(new OnResponseInfoInterceptor() {
@Override
public void onResponseInfo(String url, long networkTimeMs, int statusCode) {
L.i("url " + url);
L.i("networkTimeMs " + networkTimeMs);
L.i("statusCode " + statusCode);
}
});

我是在 Application 中设置 VolleyManager 的拦截器的,其他地方如之前一样,正常调用网络请求即可。

这种统计方法不准确的原因主要有两点(前方高能,是重点):

  1. 上面这种做法,乍一看没什么问题,但是如果对 Volley 的工作原理有一定了解的话,就会明白其实用这种方法统计的网络请求时间并不十分准确。因为在 VolleyRequestQueue 类中维护着几个队列,包括:网络请求队列、缓存请求队列、一个正在进行但是尚未完成的请求集合和一个等待请求的集合,如果向 RequestQueue 添加一个网络请求,首先会进入队列,然后等待进行真正的网络请求。所以如果像上面这种的做法的话,会把网络请求在队列中排队的时间也计算在内。
  2. 如果有多个线程在短时间内进行多次网络请求的话,都会去调用 addStringRequest() 方法,这样 startTimeStamp 有可能会出现错乱的问题,多个网络请求开始的时间和结束的时间不能一一对应,这也是一个问题。

综上所述,上面这种统计方法并不准确(如果有不同的想法和见解,欢迎沟通交流 jiankunli24@gmail.com)。

改进后更加准确的统计时长的方法

改进后的方法,在使用时并没有什么区别,不同的是在 VolleyManager 中对网络请求相关信息统计的方法和之前的不一样了。

之前在看 Volley 源码的时候,无意间看到在 VolleyNetworkResponse 中有 networkTimeMs 这样一个属性,看了其英文注释,我明白了这个 networkTimeMs 属性就代表着一个网络请求的时间。既然 Volley 已经记录了网络请求耗时,我们只需要通过一定的方法将 networkTimeMs 暴露给开发者,供开发者可以获取到即可。

新的统计方法需要继承 StringRequestJsonObjectRequestJsonArrayRequestImageRequest,重写其中的几个方法,生成其子类。拿 StringRequest 举例,自定义的 CustomStringRequest 类,如下所示:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 在 StringRequest 中,{@link deliverResponse(String)} 方法和 {@link deliverError(VolleyError)} 方法,只会调用其中的一个
public class CustomStringRequest extends StringRequest {
// 网络请求耗时
private long mNetworkTimeMs = 0L;
public CustomStringRequest(int method, String url, Response.Listener<String> listener, Response.ErrorListener errorListener) {
super(method, url, listener, errorListener);
}
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
if (response != null) {
// 若网络请求正确,则在 NetworkResponse 类中含有网络请求耗时的属性:networkTimeMs
mNetworkTimeMs = response.networkTimeMs;
}
return super.parseNetworkResponse(response);
}
@Override
protected void deliverResponse(String response) {
super.deliverResponse(response);
// 如果已经调用了这个方法,则表明网络请求成功。在 parseNetworkResponse(NetworkResponse) 方法中,为 mNetworkTimeMs 赋值
if (mNetworkTimeMs > 0) {
this.onResponseTimeAndCode(mNetworkTimeMs, 1);
}
}
@Override
public void deliverError(VolleyError error) {
super.deliverError(error);
// 如果调用了这个方法,则表明网络请求失败时。若 VolleyError 中的 networkResponse 不为空,则 networkResponse 中的
// networkTimeMs 和 statusCode 表示网络请求耗时和 Http 状态码;若 networkResponse 为空,则
// VolleyError 中的 getNetworkTimeMs() 方法可以获得网络请求耗时。
NetworkResponse response = error.networkResponse;
if (response != null) {
this.onResponseTimeAndCode(response.networkTimeMs, response.statusCode);
} else {
// Http 协议中 417 表示 Expectation Failed
this.onResponseTimeAndCode(error.getNetworkTimeMs(), 417);
}
}
/**
* 在子类中重写此方法,即可得到网络请求耗时和网络请求结果
*
* @param networkTimeMs 网络请求耗时,单位:毫秒
* @param statusCode 网络请求结果,成功则为1;失败则是具体的Http状态码,如404,500等(容易定位到请求失败的原因)
*/
protected void onResponseTimeAndCode(long networkTimeMs, int statusCode) {
}
}

自定义好 Custom***Request 之后,在 VolleyManager 中需要使用自定义的 Custom***Request,所以需要对 VolleyManager 做一定的修改,如下所示:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class VolleyManager {
......
public void addStringRequest(String url, final OnHttpListener httpListener) {
this.addStringRequest(Request.Method.GET, url, httpListener);
}
public void addStringRequest(int method, final String url, final OnHttpListener<String> httpListener) {
this.addStringRequest(method, url, httpListener, mResponseInfoInterceptor);
}
public void addStringRequest(int method, final String url,
final OnHttpListener<String> httpListener, final OnResponseInfoInterceptor interceptor) {
StringRequest request = new CustomStringRequest(method, url, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
httpListener.onSuccess(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
httpListener.onError(error);
}
}) {
@Override
protected void onResponseTimeAndCode(long networkTimeMs, int statusCode) {
sendResponseInfo(url, networkTimeMs, statusCode, interceptor);
}
};
addRequest(request);
}
/**
* 处理 OnResponseInfoInterceptor 拦截器
*
* @param url 网络请求对应的URL
* @param startTimeStamp 网络请求开始的时间,用于计算网络请求耗时
* @param statusCode 网络请求结果的状态,1表示网络请求成功,0表示网络请求失败
* @param interceptor 拦截器 {@link OnResponseInfoInterceptor} ,有一个默认的拦截器 mResponseInfoInterceptor,用户也可以通过{@link #addStringRequest(int, String,
* OnHttpListener, OnResponseInfoInterceptor)} 给网络请求设置单独的拦截器
*/
private void handleInterceptor(String url, long startTimeStamp, int statusCode,
OnResponseInfoInterceptor interceptor) {
long apiDuration = SystemClock.elapsedRealtime() - startTimeStamp;
if (apiDuration < 0 || TextUtils.isEmpty(url) || interceptor == null) {
return;
}
interceptor.onResponseInfo(url, apiDuration, statusCode);
}
......
}

还是以 StringRequest 为例,在使用自定义的 CustomStringRequest 时,需要重写其中的 onResponseTimeAndCode(long, int),并使用 handleInterceptor(String , long , int , OnResponseInfoInterceptor ) 处理得到的网络请求耗时和请求结果的状态码。

通过使用这种方法,可以很好的解决第一种统计方法的两个问题:

  1. 因为是通过 Volley 中的属性 networkTimeMs 得到的网络请求耗时,如果分析源码的话,会发现 networkTimeMs 就是在网络请求开始时记录开始时间,在网络请求结束时记录结束时间,从而得到网络请求耗时,不包括请求排队的时间,所以统计更准确。
  2. 不管有多少个线程进行网络请求,每发送一个请求,就会创建一个 CustomStringRequest 对象,而 mNetworkTimeMs 是每个 CustomStringRequest 对象的一个属性,所以并不会出现错乱的问题。
  3. 如果请求成功,statusCode 则为1;如果请求失败,statusCode 则为具体的网络请求状态码,这样更容易定位问题所在。

至此,关于Volley 源码解析及对 Volley 的扩展系列的第一篇文章就结束了,在第二篇文章中会对 Volley 的源码进行分析,解释通过第二种方法可以更准确的统计到网络请求耗时的原因。如果有什么问题欢迎指出。我的工作邮箱:jiankunli24@gmail.com