OkHttp3 使用详解及网络连接缓存的处理机制

1. 创建 OkHttpClient 对象


可以直接新建,也可以用建造者模式建造出来。直接新建时,其实也是使用建造者设置了默认的请求参数。

OkHttpClient client = new OkHttpClient();

OkHttpClient client = new OkHttpClient.Builder()
        .connectTimeout(5, TimeUnit.SECONDS)
        .writeTimeout(1000, TimeUnit.SECONDS)
        .readTimeout(1000, TimeUnit.SECONDS)
        .build();
功能 函数 注释
添加应用拦截器 addInterceptor(Interceptor) 最接近应用的拦截器
添加网络拦截器 addNetworkInterceptor(Interceptor) 接近网络的拦截器
设置缓存对象 cache(Cache cache) 使用 DiskLruCache 实现

okhttp3.Cache 构造时只需要指定缓存目录和大小即可。

网络拦截器实例:给返回的响应添加缓存过期时间

private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Response originResponse = chain.proceed(chain.request());
        // 5 分钟后过期
        CacheControl.Builder builder = new CacheControl.Builder()
                .maxAge(5, TimeUnit.MINUTES);

        return originResponse.newBuilder()
                .header("Cache-Control", builder.build().toString())
                .build();
    }
};


2. 准备好 Request 对象


通常用建造者模式来建造。

new Request.Builder().build();
功能 函数 注释
添加 URL url(String url)
GET 请求 get()
POST 方式提交表单 post(RequestBody body)
替换请求头 header(String name, String value) 先删除 name 对应的原有的字段对,再添加
添加请求头 addHeader(String name, String value) 直接往 ArrayList 中添加两个 String

表单的构建

new FormBody.Builder().build()
功能 函数 注释
添加表单项 add(String name, String value) 没有 encode 的字符串
添加表单项 addEncoded(String name, String value) 已经 encode 的字符串


3. 发送请求


OkHttpClient 实例先用 newCall(Request) 方法对请求再一次包装,返回 Call 对象,表示请求已经准备好。然后有同步和异步两种方式来发送请求:

同步方式:

Response response = client.newCall(request).execute();

异步方式:

Response response = client.newCall(request).enqueue(callback);

异步方式会用一个 CachedThreadPool 线程池来管理异步任务,任务一提交就马上在非核心线程中执行,非核心线程的空闲存活期是 60s。

  1. enqueue 方法会传入一个 okhttp3.Callback 对象,需要注意的是回调时还是在子线程。
new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        // 通过runOnUiThread()方法回到主线程处理逻辑
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                closeProgressDialog();
                Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show();
            }
        });
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

    }
}
  1. enqueue 方法中,callback 被包装成实现了 Runnable 接口的 AsyncCall 对象,传入 OkHttpClient 的 Dispatcher 。Dispatcher 会使用线程池执行 AsyncCall 的 execute 方法,最终的执行结果回调 Callback。
@Override protected void execute() {
  boolean signalledCallback = false;
  try {
    Response response = getResponseWithInterceptorChain();
    if (retryAndFollowUpInterceptor.isCanceled()) {
      signalledCallback = true;
      responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
    } else {
      signalledCallback = true;
      responseCallback.onResponse(RealCall.this, response);
    }
  }


4. 拦截器


不论是 execute 方法还是 enqueue 方法发送链接请求,最终都会调用 getResponseWithInterceptorChain 方法。

Response getResponseWithInterceptorChain() throws IOException {
  // Build a full stack of interceptors.
  List<Interceptor> interceptors = new ArrayList<>();
  interceptors.addAll(client.interceptors());
  interceptors.add(retryAndFollowUpInterceptor);
  interceptors.add(new BridgeInterceptor(client.cookieJar()));
  interceptors.add(new CacheInterceptor(client.internalCache()));
  interceptors.add(new ConnectInterceptor(client));
  if (!forWebSocket) {
    interceptors.addAll(client.networkInterceptors());
  }
  interceptors.add(new CallServerInterceptor(forWebSocket));

  Interceptor.Chain chain = new RealInterceptorChain(interceptors,
                null, null, null, 0,
                originalRequest, this, eventListener,
                client.connectTimeoutMillis(),
                client.readTimeoutMillis(),
                client.writeTimeoutMillis());

  return chain.proceed(originalRequest);
}

在请求往外发送时一次经过7个拦截器,第2~5个可以认为是 OkHttpCore 核心。

序号 名称 注释
1 应用拦截器
interceptors
由用户在创建 OkHttpClient 时设置
2 重试和重定向拦截器
RetryAndFollowUpInterceptor
1.网络请求出现异常时,如果满足重试条件就发送重试请求;
2.如果网络响应包含重定向信息,就创建重定向请求并发送
3 桥接拦截器
BridgeInterceptor
1.深加工用户传递来的请求,设置默认请求头;
2. 用户没有设置时默认采用 gzip 压缩解压数据
4 缓存拦截器
CacheInterceptor
缓存的查找和保存
5 连接拦截器
ConnectInterceptor
给网络请求提供一个连接,之后拦截器中 chain.connection() 才不为 null
6 网络拦截器
networkInterceptors
由用户在创建 OkHttpClient 时设置
7 呼叫服务器拦截器
CallServerInterceptor
网络请求最终从这里发送出去,并包装响应对象


5. CacheInterceptor 详解


网络请求中与缓存有关的头域:

序号 名称 注释
1 Cache-Control:max-age 缓存的推荐保质期
2 Cache-Control:max-stale 缓存超过保质期多久仍可接受
3 Cache-Control:min-fresh 缓存距离推荐保质期还有多久就认为缓存不新鲜了
4 Cache-Control:immutable 缓存永不过期
5 Cache-Control:only-if-cached 只允许使用当前缓存数据,没有缓存就只能返回 504 响应
6 Cache-Control:No-Cache 不希望使用缓存
7 Age 响应对象在代理缓存中存在的时间,以秒为单位
8 Date 当前的GMT时间
9 ETag 对于某个资源的某个特定版本的一个标识符
10 Expires 超过该时间则认为此回应已经过期
11 Last-Modified 服务器资源的最后修改时间

相关响应头:

序号 名称 注释
1 200 OK 合适的资源作为响应体传回客户端
2 301 Moved Permanently 所请求的 URL 资源路径已经改变,
新的 URL 在响应的 Location 头字
3 304 Not Modified 所请求的内容距离上次访问并没有
变化,浏览器缓存有效
4 404 Not Found 服务器找不到所请求的资源
5 504 Gateway TImeout 服务器作为网关不能从上游服务器
得到响应返回给客户端

下面将介绍缓存有效性相关的 3 个概念:

  1. 新鲜度检测
  2. 在验证
  3. 在验证命中

在使用缓存将 url 响应资源的副本存储在本地时,当我们再次对该 url 资源发起请求时,可以快速从本地存储中获取该 url 资源,而不需要重新发起网络连接。

但是,服务器的 url 资源可能在一定时间后被修改,因此我们不能一直使用缓存资源,在一定时间之前,认为可以使用缓存资源,在这个时间之后认为不能再使用缓存资源,需要重新请求网络资源。这个条件的判断就是 新鲜度检测。就像我们在吃食品之前要看看它有没有过期。

url 资源缓存超过一定时间,我们也不会直接重新请求 url 资源,而是去服务器查看资源是否已经发生改变,这就叫 再验证

如果服务器发现 url 资源没有改变,就返回 304 Not Modified,并不在返回对应实体,这就叫 再验证命中,此时我们可以继续使用 url 资源缓存。如果发生了变化,则返回 200 OK,并将改变后的 url 资源返回。

具体实现:

  1. 新鲜度检测

    http1.1 规范中,通过响应首部的 Cache-Control:max-age 和 Last-Modified 计算出绝对时间。http1.0 规范中,响应首部 Expires 的值就是绝对时间。

  2. 再验证

    超过绝对时间后要进行再验证,需要根据响应首部具体的规范来进行 条件请求,通常有 5 种条件请求首部。

序号 名称 注释
1 If-Modified-Since 和响应首部 Last-Modified 配合使用,询问
服务器 url 资源的最后修改时间是否变化,
没有的话就返回 304 未修改
2 If-None-Match 和响应首部 Etag 配合使用,Etag 相当于服务器
对 url 资源定义的粗粒度的版本号,即使资源修
改了,但如果版本号没有变化,仍然认为再验证
命中。
3 If-Unmodified-Since
4 If-Range
5 If-Match

在 CacheInterceptor 源码中,首先获取缓存的 Response 对象(可能为 null)。然后用缓存的 Response,以及当前的时间和 Request 构建 CacheStrategy 对象。

CacheStrategy strategy = new CacheStrategy
            .Factory(now, chain.request(), cacheCandidate).get();

CacheStrategy 有 networkRequestcacheResponse 两个变量,根据工厂方法传入的三个参数,get 方法中有 5 种判断条件来设置它们的值。

时序 条件 networkRequest cacheResponse
1 没有缓存时
2. 请求采用 https 但是缓存没有进行握手的数据
3. 缓存不应该被保存(保留了一些不应该缓存的数据)
4. 请求添加了 Cache-Control:No-Cache
或者一些条件请求首部,说明不希望使用缓存
传入的 resquest null
2 缓存响应首部包含 Cache-Control:immutable
(不属于 http 协议),说明资源不会改变
null 传入的缓存响应
3 新鲜度验证通过 null 传入的缓存响应
(可能会添加一些首部)
4 新鲜度验证不通过,使用 Etag 或 Last-Modified
或 Date 首部构造条件请求并返回
条件请求 传入的缓存响应
5 新鲜度验证不通过,且缓存响应没有 Etag、
Last-Modified 和 Date 中的任何一个
传入的 resquest null
6 上述 5 种情况中 networkRequest 不为空时,若
请求通过 Cache-Control:only-if-cached 只允许
我们使用当前缓存
null null

在 CacheInterceptor 的 intercept 方法中,会根据创建的 CacheStrategy 对象的两个变量的值来进行处理。

时序 networkRequest cacheResponse 处理方法
1 null null 直接返回 504 Unsatisfiable Request (only-if-cached)
响应。
2 null 非 null 说明缓存有效,直接返回 cacheResponse
3 非 null null 或
非 null
说明需要向网络发送请求(原始 request 或新鲜度验证后
的条件请求):
1. 如果有缓存数据,在获得再验证的响应后,使用 cache
的 update 方法更新缓存;
2. 如果没有缓存数据,判断请求是否可以被缓存,可以的
话就使用 cache 的 put 方法缓存下来


参考资料


  1. HTTP消息头(HTTP headers)-常用的HTTP请求头与响应头
  2. HTTP 必知必会的那些

猜你喜欢

转载自blog.csdn.net/weixin_40255793/article/details/81018358