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

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

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

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

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

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 请求的例子,其他 JsonObjectJsonArrayBitmap 请求的例子也是一样的。

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

// 通过 {@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 类,如下所示:

// 在 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 ) 处理得到的网络请求耗时和请求结果的状态码。

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容