0x4、缓存设计
首先,缓存适用于那些不会更新服务端数据的请求,所以一般是缓存GET请求,很少对POST请求进行缓存。
其次,缓存是非必要的,但加了会带来两个好处:
- 客户端/浏览器 → 减少网络延迟,加快页面打开速度;
- 后台 → 减少带宽消耗,降低服务器压力;
在扒Volley缓存的实现细节前,我们试试自己来设计一个缓存,从一个简陋的方案开始:
- 键值对集合存储,URL做Key,缓存(响应结果)为Value;
- 执行请求前,先到集合里根据URL查,查到直接返回缓存,查不到执行请求,请求结束后存起来;
问题来了:后台的资源是会变的,上一秒是这个,下一秒是那个,此时客户端还用本地缓存,结果可能是不对的!
- 后台配合,给资源加上有效期,可以是 直接过期时间,也可以是 资源生产时间 + 有效时长,客户端缓存的时候绑定上这个;
- 客户端请求时,如果命中缓存,校验缓存是否在有效期内,在直接返回缓存,没有则执行请求;
问题又来了:后台资源变化,客户端是无感知的,以为缓存还在有效期内,不会去请求新数据。
- 后台变化,主动通知客户端显然不太行 (硬要主动推送让客户端刷新也可以);
- 客户端还是得主动去请求下后台;
- 后台为资源添加上 标记,第一次请求时丢给客户端,客户端下次请求相关链接时丢给后台,后台判断TAG和自己算的是否相等,不等说明资源发生改变,给予客户端不同的反馈。比如:没发生改变,只返回一个304的状态码,发生改变,返回 200的状态码 + 新的响应,客户端根据状态码,决定是读缓存,还是解析新响应。这样还有个好处:后台的资源有可能一直没变,虽然过了有效期,但是没必要刷新,这样客户端也不需要更新缓存。
2333,上面的流程其实就是 保证HTTP缓存一致性 的部分思路,上面说到的 附加信息 都放在 信息报头 中:
HTTP缓存由多种规则,根据是否需要重新向服务器发起请求来分类,分为两大类:
- 强制缓存 → 缓存数据未失效,直接使用缓存数据,用响应头中的 Expires/Cache-Control 来标明失效规则;
- 对比缓存 → 第一次请求时后台将缓存标识一起返回给客户端,客户端再次请求数据时,将备份的缓存标识发送给服务器,服务器根据标识进行判断,返回304代表客户端可以使用缓存数据,其他则说明不能使用缓存数据。然后这个缓存标识一般分两种:资源修改时间 → Last-Modified/If-Modified-Since,后台生成的资源标识 → Etag/If-None-Match,后者优先级高于前者。
两类缓存可以混合使用!具体的HTTP缓存策略流程图如下:
笔者对此也只知道个大概,感兴趣想深入了解的可见:《浏览器 HTTP 协议缓存机制详解 》
Volley中也是这样的缓存策略:
接着看看缓存的定义 Cache
接口,包含了一个实体缓存实体 Entity
和缓存相关的操作方法。
跟到具体实现类 DiskBasedCache
,关注缓存增删,先看看 put()
:
简单说下流程:将消息报头和响应数据依次写入文件,然后存一个消息报头到缓存集合中,请求时判定缓存是否可用时用到。接着看看 get()
:
很好理解,缓存命中后,响应实体从流 → 字节数组 → Entity实例返回。其它情况返回Null,表示没有缓存可用。其它方法先不看了,接着看看这个 CacheKey 是怎么设计的?
跟下哪里调的put()方法 → NetworkDispatcher → processRequest()
:
跟下 Request → getCacheKey()
:
所以Key就是两种:
- GET || DEPRECATED_GET_OR_POST → 直接url
- 其它 → 请求方式数值-url
直接URL做Key,真·简单粗暴啊!
0x5、请求处理细节
回到Volley的入口方法 newRequestQueue()
,啧啧,扩展性体现之一:
HurlStack
对应HttpUrlConnection的封装,HttpClientStack
对应HttpClient的封装,还可以自行定制,继承 BaseHttpStack
类,重写 executeRequest()
返回处理后的响应即可。
另外,这里用 代理模式
套了一层,代理类是 BasicNetwork
,在 HttpClientStack
的基础上做一些附加操作。
所以,这个附加操作就是:获取缓存相关的请求头,对响应结果/异常的处理。然后,看到异常重试都是调用的 attemptRetryOnException()
:
跟下 RetryPolicy
发现是一个接口:
看下哪里实现了这个接口,跟到默认实现类 DefaultRetryPolicy
,可以看到默认最大重试次数为1:
调用一次 retry()
计数加1,如果<=最大重试次数,抛出异常
可以看到这里只是做了一个计数,并没有进行执行请求,或者请求入队的操作,那请求是怎么重新发起的?
注意这里的死循环,以及 重试方法的层级,是在catch里的,如果此时超过重试次数,抛出异常,就能退出这个死循环。
这样不会崩溃吗?当然不会,因为在调用此方法的 NetworkDispatcher
中套了一层try-catch兜底:
当真是妙啊!!!
最后还有一个点:Volley使用 内存缓存
来存放请求获得的数据,而不是直接在内存中开辟一个区域存放。
这是为啥?因为网络请求一般会很频繁,不停创建byte[],会引起频繁的GC,间接对APP性能造成影响。所以Volley定义了 ByteArrayPool
来缓存数据。
看着有点懵是吧,我简单说说:
- 规定了缓存池的默认大小为: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的设计进行了多方面的拆解,获益良多,至少现在让我封装一个请求库,不会无从入手了。
源码难啃,但弄懂了设计的原理,就会觉得豁然开朗,妙啊,也顺应了前老大说的,品经典项目源码,如品经典名著般,沁人心脾~