由于项目开始的时间比较早,其中的网络请求使用的还是 Volley 网络请求库,不过也不是直接使用 Volley,而是对 Volley 进行了一个简单的封装。前段时间提了一个新的需求,就是需要对每次网络请求的耗时和请求结果进行统计,并上传服务器。 针对此功能需求,就引出了本系列的文章。
针对本系列博客,写了一个简单的练习 Volley 的 VolleyPractice,放在了 GitHub上,欢迎沟通交流。
本文是Volley 源码解析及对 Volley 的扩展
系列的第一篇文章,主要介绍以下内容:
- Volley 的简单使用及简单封装
- 简单却不准确的统计时长的方法
- 改进后更加准确的统计时长的方法
Volley 的简单使用及简单封装
Volley 的简单使用
Volley 使用还是比较简单方便的,相关的资料和博客有很多,而且不是本文在重点,简单放点代码,展示一个简单的 GET
请求,如下所示:
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 的简单封装
先来看看封装后,进行一次和上面相同的网络请求的代码:
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
是一个回调接口,用于回调请求的结果,由于有 成功
和 失败
两种结果,所以有两个回调方法。如下代码所示:
public interface OnHttpListener<T> {
void onSuccess(T result);
void onError(VolleyError error);
}
VolleyManager
是一个管理 Volley
的类,具有如下特点:
- 其中使用单例模式,保证在整个应用生命周期内只存在一个
VolleyManager
对象 - 使用弱引用的方式持有
Context
对象,防止造成内存泄露 - 在
VolleyManager
中只有一个RequestQueue
对象,以免创建多个RequestQueue
对象,从而造成资源的浪费
VolleyManager
的代码如下所示:
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 对应起来。
根据上面的想法,创建一个回调接口,如下所示:
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
中发送网络请求方法稍微进行修改,如下所示:
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
请求的例子,其他 JsonObject
、JsonArray
和 Bitmap
请求的例子也是一样的。
在业务代码中像下面这样使用:
// 通过 {@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
的拦截器的,其他地方如之前一样,正常调用网络请求即可。
这种统计方法不准确的原因主要有两点(前方高能,是重点):
- 上面这种做法,乍一看没什么问题,但是如果对
Volley
的工作原理有一定了解的话,就会明白其实用这种方法统计的网络请求时间并不十分准确。因为在Volley
的RequestQueue
类中维护着几个队列,包括:网络请求队列、缓存请求队列、一个正在进行但是尚未完成的请求集合和一个等待请求的集合,如果向RequestQueue
添加一个网络请求,首先会进入队列,然后等待进行真正的网络请求。所以如果像上面这种的做法的话,会把网络请求在队列中排队的时间也计算在内。 - 如果有多个线程在短时间内进行多次网络请求的话,都会去调用
addStringRequest()
方法,这样startTimeStamp
有可能会出现错乱的问题,多个网络请求开始的时间和结束的时间不能一一对应,这也是一个问题。
综上所述,上面这种统计方法并不准确(如果有不同的想法和见解,欢迎沟通交流 jiankunli24@gmail.com
)。
改进后更加准确的统计时长的方法
改进后的方法,在使用时并没有什么区别,不同的是在 VolleyManager
中对网络请求相关信息统计的方法和之前的不一样了。
之前在看 Volley
源码的时候,无意间看到在 Volley
的 NetworkResponse
中有 networkTimeMs
这样一个属性,看了其英文注释,我明白了这个 networkTimeMs
属性就代表着一个网络请求的时间。既然 Volley
已经记录了网络请求耗时,我们只需要通过一定的方法将 networkTimeMs
暴露给开发者,供开发者可以获取到即可。
新的统计方法需要继承 StringRequest
、JsonObjectRequest
、JsonArrayRequest
和 ImageRequest
,重写其中的几个方法,生成其子类。拿 StringRequest
举例,自定义的 CustomStringRequest
类,如下所示:
// 在 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
做一定的修改,如下所示:
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 )
处理得到的网络请求耗时和请求结果的状态码。
通过使用这种方法,可以很好的解决第一种统计方法的两个问题:
- 因为是通过
Volley
中的属性networkTimeMs
得到的网络请求耗时,如果分析源码的话,会发现networkTimeMs
就是在网络请求开始时记录开始时间,在网络请求结束时记录结束时间,从而得到网络请求耗时,不包括请求排队的时间,所以统计更准确。 - 不管有多少个线程进行网络请求,每发送一个请求,就会创建一个
CustomStringRequest
对象,而mNetworkTimeMs
是每个CustomStringRequest
对象的一个属性,所以并不会出现错乱的问题。 - 如果请求成功,
statusCode
则为1;如果请求失败,statusCode
则为具体的网络请求状态码,这样更容易定位问题所在。
至此,关于Volley 源码解析及对 Volley 的扩展
系列的第一篇文章就结束了,在第二篇文章中会对 Volley
的源码进行分析,解释通过第二种方法可以更准确的统计到网络请求耗时的原因。如果有什么问题欢迎指出。我的工作邮箱:jiankunli24@gmail.com