最近相对空闲,于是利用这段时间看了okhttp。之前在项目中,采用过许多的网络框架。有自己写的,有用android-async-http,最近用的是volley。应该说,除了自己写的,都是非常优秀的调用框架。前段时间,学长推荐我使用okhttp,说用了就会爱不释手的。自己水平有限,写这篇文章出于自我的探索,看了网上一堆牛人的解释,自己也写写心得罢了。(我应该会不定期的更新,因为是个人的摸索,谈不上分析,所以题目这样取比较合理吧)
其实,在android studio中已经有okhttp了
我一开始没太注意版本,2.0.0的已经挺久的了,我去github上下了最新的版本是3.2.0的,前后一对比,发现更改的地方还行挺多的,我也不详细说了。(同步请求添加了队列、call类改成接口类,具体实现交给了RealCall类、添加了拦截器链等)
(1)简单用法
我想先从用法入手,这样比较容易理解。
主要的类OkHttpClient,注解是这么说的
从官方的描述里,总结一下OkHttpClient类:
1、帮助构造Call类(用来发送请求和获得返参信息)。
2、一般一个应用使用一个OkHttpClient就可以。
3、创建方法有默认的new OkHttpClient(),或者使用Builder来进行自己配置。如果是要调整已有的OkHttpClient,可以使用newBuilder()获得当前的配置信息。(例子给出的就是一个调整配置获得新的client)
so,我得出通俗的做法就是在Application类的onCreate里进行OkHttpClient的创建和具体配置(对外提供单例方法)。
于是,我尝试几个简单的例子(1、异步的post ;2、同步get)
以上便是最最最最简单的使用okhttp的例子。(可以看我用handler,也猜出他的回调是在子线程里,这里我觉得不是特别好。比如在volley里最后会调用Executor.execute方法来使回调在主线程中)下面将一遍分析一遍改进。
(2)同步的流程(初窥)
既然调用成功了,我必然要看看里面的乾坤。从同步的方法看相对会简单一点。
1、首先是调用了OkHttpClient的 newCall(Request request)方法获得一个RealCall类。它是实现了Call接口(只能请求一次,可以理解为一次性用品吧)
2、代码直接调用call.execute()方法。
首先判断是否已经使用过。接着就是调用client的dispather类的executed(这里只是把请求放入请求队列中,之后分析吧)。接着调用getResponseWithInterceptorChain方法(从名字里就知道会有拦截器链在里头)。获得的返参后就从队列中把当前的Call清除。
(3)异步的流程(初窥)
异步方法,前面的都一样。之后调用client的dispatcher.enqueue,并传入回调类。
在diapatcher中判断,队列是否到达最大数量或者同一host是否已饱和。
还有容量,就放入执行队列并直接执行(AsyncCall的execute方法,AsyncCall的父类NamedRunnable的run里调用execute)。否,则放入等待队列中。
在其中,也同样是调用getResponseWithInterceptorChain方法。根据回调来判断触发相应的回调方法,并移除call。
(4)拦截器
通过查看,明显在同步与异步中,最终都是调用了getResponseWithInterceptorChain方法。于是,本着理解的目的继续查看。
private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
return chain.proceed(originalRequest);
}
发现操作意外得简单,构造一个拦截器链并调用链的proceed方法。
@Override public Response proceed(Request request) throws IOException {
// If there's another interceptor in the chain, call that.
if (index < client.interceptors().size()) {
Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
Interceptor interceptor = client.interceptors().get(index);
Response interceptedResponse = interceptor.intercept(chain);
if (interceptedResponse == null) {
throw new NullPointerException("application interceptor " + interceptor + " returned null");
}
return interceptedResponse;
}
// No more interceptors. Do HTTP.
return getResponse(request, forWebSocket);
}
起初我不太理解,感觉应该是一个一个的拦截器操作才对,这样写感觉很奇怪。我觉得他大概是以类似递归的形式展开。看了后,网上例子都是在调用拦截器是,返参都是chain.proceed,调用下一个拦截器。这样每一个拦截器调用完后,都会紧接着调用下一个拦截器,直到全部调用过。
举一个官方例子
public void networkInterceptorsCanChangeRequestMethodFromGetToPost() throws Exception {
server.enqueue(new MockResponse());
Interceptor interceptor = new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
MediaType mediaType = MediaType.parse("text/plain");
RequestBody body = RequestBody.create(mediaType, "abc");
return chain.proceed(originalRequest.newBuilder()
.method("POST", body)
.header("Content-Type", mediaType.toString())
.header("Content-Length", Long.toString(body.contentLength()))
.build());
}
};
(5)关于网上的拦截器运用Gzip
Http协议上的gzip编码是一种用于改进Web应用程序性能的技术。运用gzip压缩技术,一是减少储存空间,二是减少传输时间。
网上关于在okhttp中使用gzip是
@Override
public Response intercept(Chain chain) throws IOException {
//从链中获得原始请求
Request originalRequest = chain.request();
//如果没有需要压缩的内容
if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
return chain.proceed(originalRequest);
}
//压缩
Request compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding", "gzip")
.method(originalRequest.method(), gzip(originalRequest.body()))
.build();
//传递给下一个
return chain.proceed(compressedRequest);
}
private RequestBody gzip(final RequestBody body) {
return new RequestBody() {
@Override public MediaType contentType() {
return body.contentType();
}
@Override public long contentLength() {
return -1; // 无法知道压缩后的数据大小
}
@Override public void writeTo(BufferedSink sink) throws IOException {
BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
body.writeTo(gzipSink);
gzipSink.close();
}
};
}
但我去github上查找时,有外国朋友写的,我觉得可以。他是在上面的基础上再次获得数据的长度。
class GzipRequestInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
return chain.proceed(originalRequest);
}
Request compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding", "gzip")
.method(originalRequest.method(), forceContentLength(gzip(originalRequest.body())))
.build();
return chain.proceed(compressedRequest);
}
/** https://github.com/square/okhttp/issues/350 */
private RequestBody forceContentLength(final RequestBody requestBody) throws IOException {
final Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
return new RequestBody() {
@Override
public MediaType contentType() {
return requestBody.contentType();
}
@Override
public long contentLength() {
return buffer.size();
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
sink.write(buffer.snapshot());
}
};
}
private RequestBody gzip(final RequestBody body) {
return new RequestBody() {
@Override public MediaType contentType() {
return body.contentType();
}
@Override public long contentLength() {
return -1; // We don't know the compressed length in advance!
}
@Override public void writeTo(BufferedSink sink) throws IOException {
BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
body.writeTo(gzipSink);
gzipSink.close();
}
};
}
}
(6)getResponse方法
遍历完拦截器链后,直接调用getResponse方法。大致步骤:
1、根据请求数据添加header信息。
2、创建HttpEngine类。
3、调用HttpEngine.sendRequest发送数据
4、调用HttpEngine.readResponse解析返回数据
5、释放连接
6、HttpEngine.getResponse获得返回信息
7、engine.followUpRequest();常看返回信息是否包含在需要处理的code范围内
8、在内则进行相应处理,否则返回结果
(7)探索HttpEngine
此类处理单一的 HTTP 请求/响应对。每个 HttpEngine遵循这个生命周期:
1、创建
2、sendRequest()时发送请求。(会判断是调网络还是本地存储的,)
3、readResponse()之后,可以获得返回信息的header和body。并且所有的response都有一个response body input stream。
在浏览sendRequest时,它首先获得本地的对应缓存,根据策略,如果不网络也不本地,会返回504。如果本地,则返回本地的。如果是网络, connect()获得连接,之后传数据。而readResponse是将httpStream.writeRequestHeaders和httpStream.writeRequestBody,将数据写入端口,之后读取返回数据readNetworkResponse。
(8)本地缓存
okhttp在发送前先解析请求,获得其类型:是调用网络还是本地。在HttpEngine.sendRequest方法中,先判断和预处理了header后,
InternalCache responseCache = Internal.instance.internalCache(client);
Response cacheCandidate = responseCache != null
? responseCache.get(request)
: null;
明显应该是获得本地的缓存信息。Internal.instance在OKhttpclient创建时就静态创建了。
InternalCache internalCache() {
return cache != null ? cache.internalCache : internalCache;
}
判断,有cache就用它的internalCache 否则就用Client的(就是默认的null)。根据这个判断,我大致猜测,cache应该是用户的缓存配置(否则cache为什么默认也是null)。OkHttpClient也给出了cache的设置方法:在builder类里有
/** Sets the response cache to be used to read and write cached responses. */
void setInternalCache(InternalCache internalCache) {
this.internalCache = internalCache;
this.cache = null;
}
public Builder cache(Cache cache) {
this.cache = cache;
this.internalCache = null;
return this;
}
看到这里,傻子都知道要设置本地缓存的方法了 == :
private static OkHttpClient instance;
/**
* 获得Client单例
*/
public static OkHttpClient getInstance() {
if (instance == null){
synchronized (OkhttpUtil.class){
if (instance == null){
instance = new OkHttpClient();
File cacheDirectory = FileUtil.getCacheDir("OkHttpCache");
if (cacheDirectory != null) {
int cacheSize = 10 * 1024 * 1024; // 10 MiB File
instance.newBuilder().cache(new Cache(cacheDirectory, cacheSize));
}
}
}
}
return instance;
}
以上就是如何设置缓存的路径了(具体文件创建就不贴出了)。接下来就是如何设置请求的存储类型了。于是接着从源码看呗。
long now = System.currentTimeMillis();
cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
networkRequest = cacheStrategy.networkRequest;
cacheResponse = cacheStrategy.cacheResponse;
代码紧接着就是分析请求的指向目标。
Factory方法创建CacheStrategy类。如果上一段代码的缓存信息不为空,就修改存储数据的一些必要信息(比如最近一次修改)。
而get方法就是获得请求相依的参数(是否请求本地缓存,还是只调接口不缓存等等。。)代码太多就不贴了。但是这句代码吸引了我。
CacheControl requestCaching = request.cacheControl();
他就是获得每个request的请求缓存参数。这下就明白,每个单独的请求可以设置其cacheControl。于是
private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds,
boolean isPrivate, boolean isPublic, boolean mustRevalidate, int maxStaleSeconds,
int minFreshSeconds, boolean onlyIfCached, boolean noTransform, String headerValue) {
this.noCache = noCache;
this.noStore = noStore;
this.maxAgeSeconds = maxAgeSeconds;
this.sMaxAgeSeconds = sMaxAgeSeconds;
this.isPrivate = isPrivate;
this.isPublic = isPublic;
this.mustRevalidate = mustRevalidate;
this.maxStaleSeconds = maxStaleSeconds;
this.minFreshSeconds = minFreshSeconds;
this.onlyIfCached = onlyIfCached;
this.noTransform = noTransform;
this.headerValue = headerValue;
}
一看参数太多了。。。。。没事,因为CacheControl的参数太多,源码已经给了几个常用的。一个是FORCE_NETWORK(只请求网络)、FORCE_CACHE(只请求缓存)。默认情况自然是都会操作。
//创建一个Request
final Request request = new Request.Builder()
.cacheControl(CacheControl.FORCE_NETWORK)
.url("http://xxxxxxxxxxxxx")
.post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonObject.toString()))
.build();