likes
comments
collection
share

【源码】"拆" 网络请求库-Volley

作者站长头像
站长
· 阅读数 45

0x1、引言

Volley跟AsyncTask一样,都是老古董了,最早发布于2013年的Google I/O大会,初衷就是:让Android开发者少写重复的请求代码

【源码】"拆" 网络请求库-Volley

怎么说?早期网络请求都是用 HttpURLConnection 或者 HttpClient,直接用非常麻烦,比如一段请求百度的代码:

private void sendRequest() {
        //开线程发起网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection conn = null;
                BufferedReader reader = null;
                try {
                    //获取HttpRULConnection实例
                    URL url = new URL("https://www.baidu.com");
                    conn = (HttpURLConnection) url.openConnection();
                    
                    //设置请求方法和自由定制
                    conn.setRequestMethod("GET");
                    conn.setConnectTimeout(8000);
                    conn.setReadTimeout(8000);
                    
                    //获取响应码和返回的输入流
                    int i = conn.getResponseCode();
                    InputStream in = conn.getInputStream();
                    
                    //对输入流进行读取
                    reader = new BufferedReader(new InputStreamReader(in));
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        response.append(line);
                    }
                    showResponse(response.toString());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (reader != null) {
                        try {
                            reader.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if (connection != null) {
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }

    // 请求结果展示
    private void showResponse( final String response) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //进行UI操作
                xxx.setText(response);
            }
        });
    }

每个请求都是写上这样一串又长又臭的玩意,写的人死了,看的人也死了,所以一般要封装下。

【源码】"拆" 网络请求库-Volley

封装得好也不简单,得去考虑并发,请求取消等等。而Volley所做的事情也是封装,将复杂通信细节封装在内部,开发者仅需写少量代码,即可完成网络请求。

Github仓库,Google竟然还有在维护这个库,而且有API文档 《Volley 概览》,文档中介绍了它的一些优势:

【源码】"拆" 网络请求库-Volley

使用方法很简单,先在build.gradle中添加依赖:

implementation 'com.android.volley:volley:1.1.1'

然后写段简单的请求示例(官方示例有Kotlin版~)

// 创建请求队列
RequestQueue mQueue = Volley.newRequestQueue(context);

// 构造请求
StringRequest stringRequest = new StringRequest("http://www.baidu.com",
	new Response.Listener<String>() {
		@Override
		public void onResponse(String response) {
			Log.d("TAG", response);
		}
	}, new Response.ErrorListener() {
		@Override
		public void onErrorResponse(VolleyError error) {
			Log.e("TAG", error.getMessage(), error);
		}
});

// 将请求加入队列
mQueue.add(stringRequest);

相比起上面直接粗暴的写法,代码赏心悦目了不少,文档中还给出了一张Volley中请求的处理流程图:

【源码】"拆" 网络请求库-Volley

让我们大概知道,Volley中的请求,还支持缓存,命中缓存直接读缓存里的数据,不用再去请求后台。

实际业务开发中,都是OkHttp的天下了,Volley基本绝迹了,也不推荐在项目里用。但其设计思想是可以借鉴的,代码虽简五脏俱全,了解它对后续其他框架的学习也有所裨益,故本节花上亿点点时间来对他进行 拆解


0x2、并发设计

像上面HttpUrlConnection的例子,每发起一次请求,都新起一个线程,太蠢了

  • 没复用线程,频繁创建销毁线程造成不必要的开销;
  • 没对最大线程数做一个限制,可能会造成过度资源竞争,系统使用率不高;

所以涉及到并发的开源项目,线程池 基本是没得走的了,再配个 任务队列 进出队列操作加锁,有时为了解耦,还会再拆出来一个 调度器死循环 访问 任务队列,取出任务交由线程池执行。

Volley是支持并发的,看下它是如何设计的,没有找到线程池初始化代码,倒是找到了 两个线程实现类

【源码】"拆" 网络请求库-Volley

【源码】"拆" 网络请求库-Volley

NetworkDispatcher 继承 Thread类,所以这里默认定义了 容量为4的线程数组,另外一个线程 CacheDispatcher。它们都在 RequestQueue → start() 中启动:

【源码】"拆" 网络请求库-Volley

跟下start()的调用处 Volley → newRequestQueue

【源码】"拆" 网络请求库-Volley

就是实例化RequestQueue的时候,就创建并开启了这些线程。


0x3、请求调度设计

调用 RequestQueue → add() 请求入队,跟下具体实现:

【源码】"拆" 网络请求库-Volley

用到三个集合,根据对应注释推演出各种用途:

  • mCurrentRequests : HashSet → 正在处理的所有请求(包括等待和处理中)的集合;
  • mNetworkQueue : PriorityBlockingQueue → 非缓存请求队列;
  • mCacheQueue : PriorityBlockingQueue → 缓存请求队列;

入队比较简单,接着看下具体的调度流程,不难看出分成两类,走缓存(默认)和不走缓存,先看前者~

① 不走缓存的请求

NetworkDispatcher → run()

【源码】"拆" 网络请求库-Volley

【源码】"拆" 网络请求库-Volley

死循环拿队列的请求,处理请求后的结果流向有四个:

  • 请求处于取消状态 → request.finish("network-discard-cancelled");
  • 304且响应已解析过 → request.finish("not-modified");
  • 正常结束 → mDelivery.postResponse(request, response);
  • 异常结束 → mDelivery.postError(request, volleyError);

先跟下 Request → finish()

【源码】"拆" 网络请求库-Volley

【源码】"拆" 网络请求库-Volley

比较简单:移除集合中的请求,打日志,调用下请求结束的回调,接着看下后两个,跟下 ExecutorDelivery

【源码】"拆" 网络请求库-Volley

跟下 mResponsePoster 初始化部分代码:

【源码】"拆" 网络请求库-Volley

好吧,就是一个 执行器,负责将任务分发出去,因为后续操作在主线程操作,所以得用 Handler

接着,看下任务具体做了啥 ResponseDeliveryRunnable → run()

【源码】"拆" 网络请求库-Volley

【源码】"拆" 网络请求库-Volley

正常请求的流转还是很好理解的,接着看下走缓存请求的调度过程~

② 走缓存的请求

直接跟 CacheDispatcher → run(),同样是死循环,直接看 processRequest() 部分代码:

【源码】"拆" 网络请求库-Volley

简单点说:有缓存且缓存没过期直接返会,其他情况把请求加到请求队列中。接着再看看缓存这块是咋设计的~


0x4、缓存设计

首先,缓存适用于那些不会更新服务端数据的请求,所以一般是缓存GET请求,很少对POST请求进行缓存。

其次,缓存是非必要的,但加了会带来两个好处:

  • 客户端/浏览器 → 减少网络延迟,加快页面打开速度;
  • 后台 → 减少带宽消耗,降低服务器压力;

在扒Volley缓存的实现细节前,我们试试自己来设计一个缓存,从一个简陋的方案开始:

  • 键值对集合存储,URL做Key,缓存(响应结果)为Value;
  • 执行请求前,先到集合里根据URL查,查到直接返回缓存,查不到执行请求,请求结束后存起来;

问题来了:后台的资源是会变的,上一秒是这个,下一秒是那个,此时客户端还用本地缓存,结果可能是不对的!

  • 后台配合,给资源加上有效期,可以是 直接过期时间,也可以是 资源生产时间 + 有效时长,客户端缓存的时候绑定上这个;
  • 客户端请求时,如果命中缓存,校验缓存是否在有效期内,在直接返回缓存,没有则执行请求;

问题又来了:后台资源变化,客户端是无感知的,以为缓存还在有效期内,不会去请求新数据。

  • 后台变化,主动通知客户端显然不太行 (硬要主动推送让客户端刷新也可以);
  • 客户端还是得主动去请求下后台;
  • 后台为资源添加上 标记,第一次请求时丢给客户端,客户端下次请求相关链接时丢给后台,后台判断TAG和自己算的是否相等,不等说明资源发生改变,给予客户端不同的反馈。比如:没发生改变,只返回一个304的状态码,发生改变,返回 200的状态码 + 新的响应,客户端根据状态码,决定是读缓存,还是解析新响应。这样还有个好处:后台的资源有可能一直没变,虽然过了有效期,但是没必要刷新,这样客户端也不需要更新缓存。

2333,上面的流程其实就是 保证HTTP缓存一致性 的部分思路,上面说到的 附加信息 都放在 信息报头 中:

【源码】"拆" 网络请求库-Volley

HTTP缓存由多种规则,根据是否需要重新向服务器发起请求来分类,分为两大类:

  • 强制缓存 → 缓存数据未失效,直接使用缓存数据,用响应头中的 Expires/Cache-Control 来标明失效规则;
  • 对比缓存 → 第一次请求时后台将缓存标识一起返回给客户端,客户端再次请求数据时,将备份的缓存标识发送给服务器,服务器根据标识进行判断,返回304代表客户端可以使用缓存数据,其他则说明不能使用缓存数据。然后这个缓存标识一般分两种:资源修改时间 → Last-Modified/If-Modified-Since后台生成的资源标识 → Etag/If-None-Match后者优先级高于前者

两类缓存可以混合使用!具体的HTTP缓存策略流程图如下:

【源码】"拆" 网络请求库-Volley

笔者对此也只知道个大概,感兴趣想深入了解的可见:《浏览器 HTTP 协议缓存机制详解 》

Volley中也是这样的缓存策略:

【源码】"拆" 网络请求库-Volley

接着看看缓存的定义 Cache 接口,包含了一个实体缓存实体 Entity 和缓存相关的操作方法。

【源码】"拆" 网络请求库-Volley

跟到具体实现类 DiskBasedCache,关注缓存增删,先看看 put()

【源码】"拆" 网络请求库-Volley

【源码】"拆" 网络请求库-Volley

简单说下流程:将消息报头和响应数据依次写入文件,然后存一个消息报头到缓存集合中,请求时判定缓存是否可用时用到。接着看看 get()

【源码】"拆" 网络请求库-Volley

很好理解,缓存命中后,响应实体从流 → 字节数组 → Entity实例返回。其它情况返回Null,表示没有缓存可用。其它方法先不看了,接着看看这个 CacheKey 是怎么设计的?

跟下哪里调的put()方法 → NetworkDispatcher → processRequest()

【源码】"拆" 网络请求库-Volley

跟下 Request → getCacheKey()

【源码】"拆" 网络请求库-Volley

所以Key就是两种:

  • GET || DEPRECATED_GET_OR_POST → 直接url
  • 其它请求方式数值-url

直接URL做Key,真·简单粗暴啊!


0x5、请求处理细节

回到Volley的入口方法 newRequestQueue(),啧啧,扩展性体现之一:

【源码】"拆" 网络请求库-Volley

HurlStack 对应HttpUrlConnection的封装,HttpClientStack 对应HttpClient的封装,还可以自行定制,继承 BaseHttpStack 类,重写 executeRequest() 返回处理后的响应即可。

另外,这里用 代理模式 套了一层,代理类是 BasicNetwork,在 HttpClientStack 的基础上做一些附加操作。

【源码】"拆" 网络请求库-Volley

【源码】"拆" 网络请求库-Volley

所以,这个附加操作就是:获取缓存相关的请求头,对响应结果/异常的处理。然后,看到异常重试都是调用的 attemptRetryOnException()

【源码】"拆" 网络请求库-Volley

跟下 RetryPolicy 发现是一个接口:

【源码】"拆" 网络请求库-Volley

看下哪里实现了这个接口,跟到默认实现类 DefaultRetryPolicy,可以看到默认最大重试次数为1:

【源码】"拆" 网络请求库-Volley

调用一次 retry() 计数加1,如果<=最大重试次数,抛出异常

【源码】"拆" 网络请求库-Volley

可以看到这里只是做了一个计数,并没有进行执行请求,或者请求入队的操作,那请求是怎么重新发起的?

【源码】"拆" 网络请求库-Volley

注意这里的死循环,以及 重试方法的层级,是在catch里的,如果此时超过重试次数,抛出异常,就能退出这个死循环。

这样不会崩溃吗?当然不会,因为在调用此方法的 NetworkDispatcher 中套了一层try-catch兜底:

【源码】"拆" 网络请求库-Volley

当真是妙啊!!!

【源码】"拆" 网络请求库-Volley

最后还有一个点:Volley使用 内存缓存 来存放请求获得的数据,而不是直接在内存中开辟一个区域存放。

这是为啥?因为网络请求一般会很频繁,不停创建byte[],会引起频繁的GC,间接对APP性能造成影响。所以Volley定义了 ByteArrayPool 来缓存数据。

【源码】"拆" 网络请求库-Volley

看着有点懵是吧,我简单说说:

  • 规定了缓存池的默认大小为:DEFAULT_POOL_SIZE = 4096字节 = 4KB,池中维护两个列表,一个按照 最近使用顺序 排序(表①),一个按照 byte[]大小 排序(表②);
  • 从缓存区取空间:不直接开辟空间,先循环迭代查找列表②中是否有 len>=所需字节 的已开辟空间,有的话返回此空间,没有的话开辟新的空间返回;
  • 将空间返还给缓存区:检查插入数据是否超出边界,有的话直接返回,没有的话添加到表①尾部,然后二分查找找到表②中合适的位置插入,当前开辟字节数增加。同时判断是否超过最大值,是回收表②的第一个元素;

0x6、可扩展性

Tips:其实看下都有哪些接口和抽象类,就知道有啥可以自定义的~

RequestQueue构造方法传入

  • Cache缓存,默认是将响应数据放磁盘中,你可以弄成二三级缓存,实现接口;
  • Network请求框架封装,默认封了HttpUrlConnection和HttpClient,弄成其他请求库也可以;
  • ResponseDelivery响应交付,就是请求响应的后续处理,正常与异常情况的处理;

Request构造方法传入

  • RetryPolicy → 请求失败重试策略;

其它

  • Request请求,toolbox里有常用的实现类,如:JsonRequest、ImageRequest、StringRequest等;
  • Authenticator令牌授权

大概就这些吧,toolbox里还送了一个图片加载的工具,不过性能不算特别好,就不展开讲了。


0x7、小结

本节对请求库Volley的设计进行了多方面的拆解,获益良多,至少现在让我封装一个请求库,不会无从入手了。

源码难啃,但弄懂了设计的原理,就会觉得豁然开朗,妙啊,也顺应了前老大说的,品经典项目源码,如品经典名著般,沁人心脾~

【源码】"拆" 网络请求库-Volley

参考文献

转载自:https://juejin.cn/post/7052982509826474020
评论
请登录