上篇文章我们讲到了getResponse()方法,这节接着来看:
getResponse()方法中最重要的有两个方法,sendRequest() 和 readResponse();
先来看 sendRequest() :
|
|
第10句:
|
|
Internal 是个抽象类, Internal.instance 对象最终在 OKhttpClient 的static 代码块中构造,
|
|
|
|
我们这里需要看下cache.internalCache的实现,这个位于Cache.Java里面。
|
|
就是一些对cache的控制,sendRequest方法中的responseCache就是Cache中的InternalCache,因此我们继续看下面的代码。
|
|
在看get方法之前我们有必要看一下缓存响应的put方法:
从上面代码可以看到,首先是对请求的方法进行判断,概括起来就是一句话:只缓存请求方法为GET的响应。然后符合缓存的条件后,使用响应创建一个Entry对象,然后使用DiskLruCache写入缓存,最终返回一个CacheRequestImpl对象。cache是DiskLruCache的实例,调用edit方法传入响应的key值,而key值就是对请求调用urlToKey方法。下面是urlToKey的实现:
|
|
从代码就可以看出是对请求的URL做MD5然后再得到MD5值的十六进制表示形式,这儿就不继续看了。
Entry实例就是要写入的缓存部分,主要看一下它的writeTo()方法,该方法执行具体的写入磁盘操作:
|
|
从上面的代码可以看到,写入缓存的不仅仅只是响应的头部信息,还包括请求的部分信息:URL、请求方法、请求头部。至此,我们看到对于一个请求和响应,缓存中的key值是请求的URL的MD5值,而value包括请求和响应部分。Entry的writeTo()方法只把请求的头部和响应的头部保存了,最关键的响应主体部分在哪里保存呢?答案在put方法的返回体CacheRequestImpl,下面是这个类的实现:
|
|
最终,通过 editor.commit();进行缓存写入。看完了put方法再来看get方法就能好理解点了。
|
|
从代码中可以看到,首先是对请求的URL进行MD5计算得到key值,然后尝试根据key值从缓存中得到值,如果没有该值,说明缓存中没有该值,那么直接返回null,否则创建Entry对象,然后再从Entry中得到响应对象,如果请求和响应不匹配(地址,请求方式、请求头) ,那么也返回null,否则就返回响应对象。
下面是Entry的构造方法:
|
|
在put方法中我们知道了缓存中保存了请求的信息和响应的信息。 获得了包含首部信息的Entry之后,再调用response方法得到正在的响应,下面是response()方法的实现:
|
|
再看下match方法:
|
|
可以看到,响应的首部信息保存在Entry中,而主体部分是在传入的Snapshot中,主体是创建了一个CacheResponseBody对象。CacheResponseBody继承自ResponseBody类并且使用传入的Snapshot获得put中保存的响应主体部分。
最终通过比较发起请求的url,方法,head等信息和缓存中的进行比较,决定是否返回response。
OK,再回到 HttpEngine 类,上面分析完了10,11行,我们再来接着看13行,
|
|
可以看到根据当前时间、构建的Request请求体、和得到的缓存响应 创建一个工厂,然后再得到一个CacheStrategy。首先看该工厂的构造方法:
|
|
从代码中可以看出,如果候选的缓存响应不为null,那么将响应首部中有关缓存的首部的值得到,主要有Date、Expires、Last-Modified、ETag和Age首部。
其中Date表明响应报文是何时创建的,Expires表示该响应的绝对过期时间,Last-Modified表示最近一次修改的时间。
再看Factory的get方法:
|
|
从代码中可以看出首先调用getCandidate()得到候选的CacheStrategy对象,然后如果得到的缓存策略表明需要使用网络,但是请求中指定响应只能从缓存中得到,那么返回一个networkRequest和cacheResonse均为null的CacheStrategy。
OkHttp中使用了CacheStrategy,它根据之前的缓存结果与当前将要发送Request的header进行策略分析,并得出是否进行请求的结论。
CacheStrategy类似一个mapping操作,将两个值输入,再将两个值输出。
> Input request, cacheCandidate
↓ ↓
> CacheStrategy 处理,判断Header信息
↓ ↓
> Output networkRequest, cacheResponse
Request:
开发者手动编写并在Interceptor中递归加工而成的对象,我们只需要知道了目前传入的Request中并没有任何关于缓存的Header。
cacheCandidate:
也就是上次与服务器交互缓存的Response,可能为null。我们现在知道它是一个可以读取缓存Header的Response。
当被CacheStrategy加工输出后,输出networkRequest与cacheResponse,根据是否为空执行不同的请求。
下面主要看一下getCandidate方法,该方法返回的策略是基于请求可以使用网络的假设之上的,所以这也就解释了get()方法中为什么要对使用网络但是请求却指定缓存响应的情况做区分。
|
|
- 上面的主要内容是缓存,okhttp实现的缓存策略实质上就是大量的if判断集合,这些是根据RFC标准文档写死的。
- Okhttp的缓存是自动完成的,完全由服务器Header决定的,自己没有必要进行控制。网上热传的文章在Interceptor中手工添加缓存代码控制,它固然有用,但是属于Hack式的利用,违反了RFC文档标准,不建议使用,OkHttp的官方缓存控制在注释中。如果读者的需求是对象持久化,建议用文件储存或者数据库即可(比如realm、litepal)。
- 服务器的配置非常重要,如果你需要减小请求次数,建议直接找对接人员对max-age等头文件进行优化;服务器的时钟需要严格NTP同步。
总结上面的方法:
如果缓存没有命中,即cacheResponse==null,那么直接进行网络请求
如果请求是HTTPS并且缓存响应中没有握手或者握手信息丢失,那么需要重新进行网络请求
如果响应不应该被存储,那么需要重新进行网络请求 (比如header中指定no-store)
如果请求中指定不使用缓存响应,那么需要进行网络请求 (比如header中指定no-cache)
接下来比较缓存响应检查是否有条件请求的首部,如果有,就进行额外的请求( request.header(“If-Modified-Since”) != null || request.header(“If-None-Match”) != null)
上面得到 CacheStrategy 后,我们下来再回到HttpEngine 定位到 17行:
|
|
|
|
可以看到该方法主要就是对Cache中的三个记录进行赋值,从这儿我们可以得出结论requestCount>=networkCount+hitCount。
当networkRequest和cacheResponse均为null的时候,这个时候的响应既不是从网络得到也不是从缓存得到。 (查阅图1)
cacheResponse表示从缓存上得到的响应。如果该响应没有使用缓存,那么将会为null。
关于Response有一点需要铭记,该类的实例不是一成不变的,响应主体部分只能被消费一次然后关闭,其他参数是不变的。
那么为什么Response的主体部分只能被消费一次呢?
这是因为ResponseBody的底层是用Okio实现的,而Okio的Source只能被读取一次,因为读完之后,Buffer底层的Segment关于之前数据的信息(pos和limit)就丢失了,并且在读完一次之后就将Source关闭了,所以只能读一次。关于Okio的可以参考拆轮子系列:拆 Okio。
再用个分割线进行分割,上面的代码得到了缓存策略,即cacheStrategy,再次定位到HttpEngine 的第37行:
|
|
|
|
跳转到 newStream 方法,
|
|
在newStream 方法中,首先调用了 findHealthyConnection,得到RealConnection对象,再根据framedConnection 是否为空,确定采取的http协议:
Http2xStream 代表是https请求,采用 http2.0或者spdy 协议。
Http1xStream 代表是http请求,采用 http1.0或者 http1.1 协议。
newStream 方法中又调用了 findHealthyConnection ,
这个方法当中又调用了 findConnection 方法:
我们再来看findConnection 方法:
|
|
重要的看下这三个方法:
前面我们已经说过了get是Internal的方法,是在OkhttpClient中的static代码块中实现的,这个get方法中最终调用了ConnectPool 的get方法,看名字我们就能猜到,这儿维护了一个连接池,这个东西:
|
|
get 和 put 都是对它的操作。
OK,看下第二个方法,connect方法,
|
|
可以看到,典型的socket连接,最终使用Okio包进行socket连接。
再底层先不看了,了解到主要流程就行。
再次回到httpEngine方法的37行,我们知道connect方法最终得到一个httpstream对象(好像OKhttp2.x版本这个方法没返回值)。
到这儿,sendRequest 方法我们就分析完了。篇幅太长了,所以我们下篇继续来看 readResponse() 方法。