OkHttp缓存机制分析和使用案例

1. OkHttp缓存机制部分源码查看

为了方便在AndroidStudio中查看源码,这里更换4.9.0版本为:

implementation 'com.squareup.okhttp3:okhttp:3.12.0'

如果需要其他的版本,可以在mvnrepository中搜索。

切换AndroidStudio的为Project视图,然后找到OkHttp,可以看到:
在这里插入图片描述

简单看下这几个类:

1.1. CacheInterceptor缓存拦截器

CacheInterceptor:缓存拦截器,负责拦截处理网络请求。注意到下面的注释:处理来自缓存的请求并将响应写入缓存。

/** Serves requests from the cache and writes responses to the cache. */
public final class CacheInterceptor implements Interceptor {
	final @Nullable InternalCache cache;
	@Override
	public Response intercept(Chain chain) throws IOException {
		// 如果本次请求缓存有数据,就直接取出
		Response cacheCandidate = cache != null
	        ? cache.get(chain.request())
	        : null;
	        
		long now = System.currentTimeMillis();

		// 将请求传入到缓存策略,得到cacheResponse
    	CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    	Request networkRequest = strategy.networkRequest;
    	Response cacheResponse = strategy.cacheResponse;
		
		if (cache != null) {
	      cache.trackResponse(strategy);
	    }
	
	    if (cacheCandidate != null && cacheResponse == null) {
	      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
	    }
		
		// If we're forbidden from using the network and the cache is insufficient, fail.
		// 请求失败,504 网关错误
    	if (networkRequest == null && cacheResponse == null) {
	      return new Response.Builder()
	          .request(chain.request())
	          .protocol(Protocol.HTTP_1_1)
	          .code(504) 
	          .message("Unsatisfiable Request (only-if-cached)")
	          .body(Util.EMPTY_RESPONSE)
	          .sentRequestAtMillis(-1L)
	          .receivedResponseAtMillis(System.currentTimeMillis())
	          .build();
    	}
		
		// If we don't need the network, we're done.
	    if (networkRequest == null) {
	      return cacheResponse.newBuilder()
	          .cacheResponse(stripBody(cacheResponse))
	          .build();
	    }
		// 缓存无效,继续执行网络请求
		Response networkResponse = null;
	    try {
	      networkResponse = chain.proceed(networkRequest);
	    } finally {
	      // If we're crashing on I/O or otherwise, don't leak the cache body.
	      if (networkResponse == null && cacheCandidate != null) {
	        closeQuietly(cacheCandidate.body());
	      }
	    }
		
		// If we have a cache response too, then we're doing a conditional get.
		// 协商缓存,如果本地存在,且资源为修改,就直接返回response
   		 if (cacheResponse != null) {
	      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
	        Response response = cacheResponse.newBuilder()
	            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
	            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
	            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
	            .cacheResponse(stripBody(cacheResponse))
	            .networkResponse(stripBody(networkResponse))
	            .build();
	        networkResponse.body().close();
	
	        // Update the cache after combining headers but before stripping the
	        // Content-Encoding header (as performed by initContentStream()).
	        cache.trackConditionalCacheHit();
	        cache.update(cacheResponse, response);
	        return response;
	      } else {
	        closeQuietly(cacheResponse.body());
	      }
	    }
		
		// 请求数据,写入cache
		...
		return response;
	}
}

1.2. CacheStrategy缓存策略类

CacheStrategy:缓存策略类。主要用来判断当前缓存时候可用。在这个类的内部类Factoryget方法中,调用getCandidate方法,这个方法会将请求进行包装,也即是在请求头部添加和缓存控制相关的头部信息,比如这里我截取部分:

// CacheStrategy.Factory->getCandidate方法
String conditionName;
String conditionValue;
if (etag != null) {
  conditionName = "If-None-Match";
  conditionValue = etag;
} else if (lastModified != null) {
  conditionName = "If-Modified-Since";
  conditionValue = lastModifiedString;
} else if (servedDate != null) {
  conditionName = "If-Modified-Since";
  conditionValue = servedDateString;
} else {
  return new CacheStrategy(request, null); // No condition! Make a regular request.
}

// 包装,添加头部信息
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

// 关联到请求的header
Request conditionalRequest = request.newBuilder()
      .headers(conditionalRequestHeaders.build())
      .build();
return new CacheStrategy(conditionalRequest, cacheResponse);

1.3. DiskLruCache磁盘缓存类

Google提供了一套硬盘缓存的解决方案:DiskLruCache(非Google官方编写,但获得官方认证)。可以参考郭大佬的Android DiskLruCache完全解析,硬盘缓存的最佳方案来了解这个类。

在前面我们在OkHttpClient创建的时候指定了cache,如:

OkHttpClient client = new OkHttpClient.Builder()
        .cache(new Cache(file, cacheSize))
        // .addInterceptor(new MyInteceptor())
        .build();

这里看下Cache这个类:

public final class Cache implements Closeable, Flushable {
	final DiskLruCache cache;
	final InternalCache internalCache = new InternalCache() {
		@Override 
		public @Nullable Response get(Request request) throws IOException {
      		return Cache.this.get(request);
    	}

	    @Override 
	    public @Nullable CacheRequest put(Response response) throws IOException {
	      return Cache.this.put(response);
	    }
	    ...
	}
}

其实Cache类底层也就是调用DiskLruCache

2. 缓存案例

在上文OkHttp的HTTP缓存使用中做了简单的使用,但是不难发现在上篇的案例中虽然能够使用HTTP缓存,但是具有一定的局限性。

因为使用OkHttpHTTP缓存时候需要确保服务端的响应是设置了对应的HTTP缓存首部控制信息。这种双方编码人员需要协商。显然这种方式不够简洁。

所以可以尝试在OkHttp客户端这边加入首部信息,而拦截器可以做这件事情。

2.1 场景:服务器响应首部没有HTTP缓存控制信息

自定义一个拦截器,添加对应请求缓存控制,也响应

class MyInteceptor implements Interceptor{

    @Override
    public Response intercept( Chain chain) throws IOException {
        // 1. 请求头部设置缓存信息
        Request request = chain.request().newBuilder()
                .cacheControl(CacheControl.FORCE_CACHE)  // 强制从缓存中读取
                .build();

        // todo
        Log.e("TAG", "intercept: ");
        // 2. 响应请求头添加
        // 移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性
        return chain.proceed(request).newBuilder()
                .removeHeader("Pragma")
                .removeHeader("Cache-Control")
                .addHeader("Cache-Control", "max-age=1800")
                .build();
    }
}

这里如果不适用强制从缓存读取,会导致虽然OkHttp每次都写入了缓存数据,但是数据却没有从本地缓存中读取。

此时的缓存文件头部:
在这里插入图片描述
和上篇OkHttp的HTTP缓存使用中的截图做对比可以发现这里写入缓存的首部信息少了Cache-ControlLast-Modified

但至于怎么让他可以有这两个字段,暂时我还不知道。

也就是暂时还不知道怎么做到理想情况:有缓存且没过期就从本地缓存中读取数据,没有缓存或者是已经过期就从服务器进行联网加载数据。

找到了解决办法。还是使用拦截器,只是在使用的时候使用的是addNetworkInterceptor,而不是使用addInterceptor

File file = new File(Environment.getExternalStorageDirectory(), "DCIM");
long cacheSize = 10 * 1024 * 1024L; // 10MB
OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(new MyInteceptor())  // 这里添加拦截器使用的是addNetworkInterceptor
        .cache(new Cache(file, cacheSize))
        .build();

CacheControl cacheControl = new CacheControl.Builder()
        .maxAge(1, TimeUnit.MINUTES)  // 缓存一分钟内有效,直接加载缓存
        .build();

Request request = new Request.Builder()
        .cacheControl(cacheControl)
        .url("http://192.168.1.110:90/test/1.0/users/1")
        .build();

button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Call call = client.newCall(request);
            call.enqueue(new Callback() {

                @Override
                public void onFailure(Call call, IOException e) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            textView.setText("数据请求失败。");
                        }
                    });
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    int code = response.code();
                    String string = response.body().string();
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            textView.setText(code+"\n"+string);
                        }
                    });
                }
            });
        }
    }
);

// MyInteceptor.java
class MyInteceptor implements Interceptor{

    @Override
    public Response intercept( Chain chain) throws IOException {
        Request request = chain.request().newBuilder()
                .build();

        // todo
        Log.e("TAG", "intercept: ");
        // 响应请求头添加
        //设置缓存时间为60秒,并移除了pragma消息头
        return chain.proceed(request).newBuilder()
                .removeHeader("Pragma")
                .addHeader("Cache-Control", "max-age=1800")
                .addHeader("Last-Modified", getTime())
                .build();
    }

    private String getTime(){
        ZonedDateTime zonedDateTime = ZonedDateTime.now().with(LocalTime.MAX);
        return zonedDateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME);
    }
}

就可以做到在一分钟内的请求,会直接从缓存中读取,而不会请求后台服务器数据。此时再次来看下缓存的数据头部信息:
在这里插入图片描述
可以发现确实将Cache-ControlLast-Modified这两个字段添加到了本地缓存头部文件中。也证实了前面的分析确实是正确的。所以这里从缓存中拿取数据才会成功。

对于addNetworkInterceptor和addInterceptor这两个方法做一个区分:

// OkHttpClient.java
public static final class Builder {
	final List<Interceptor> interceptors = new ArrayList<>();
	final List<Interceptor> networkInterceptors = new ArrayList<>();
	...
}

这两者都是List列表存储的拦截器对象。这里看不出有什么区别。不妨来看下官网的API文档:here

但是其实也没有得到解释。不过网上有很多解释,这里做一个笔记:
addInterceptor是添加应用拦截器,而addNetworkInterceptor网络拦截器
应用拦截器不关心OkHttp注入的头信息,所以在这里注册的拦截器并不会添加自己添加的响应头部信息到磁盘缓存中;
网络拦截器能够操作中间过程的响应,根据上面的案例我们知道使用这种方式可以使用本地未过期的磁盘缓存。

2.2 场景:断网强制使用缓存(服务器响应首部没有HTTP缓存控制信息)

这里就直接设置请求头的缓存控制为强制从缓存读取数据。

ps:这么做有个问题,那就是如果将缓存目录文件删除了,或者数据更新了,这里是不知道的。因为除了第一次获取数据后,强制缓存就根本不联网。

所以可以将强制读取用在断网的时候,强制从缓存中加载。其余的时候就不适用强制缓存。

File file = new File(Environment.getExternalStorageDirectory(), "DCIM");
long cacheSize = 10 * 1024 * 1024L; // 10MB
OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new MyInteceptor())  // 添加请求拦截器,以添加响应首部缓存信息
        .cache(new Cache(file, cacheSize))
        .build();

CacheControl cacheControl = new CacheControl.Builder()
        .maxAge(1, TimeUnit.MINUTES)
        .build();

Request request = new Request.Builder()
        .cacheControl(cacheControl)
        .url("http://192.168.1.102:90/test/1.0/users/1")
        .build();

button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Call call = client.newCall(request);
            call.enqueue(new Callback() {

                @Override
                public void onFailure(Call call, IOException e) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            textView.setText("数据请求失败。");
                        }
                    });
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    int code = response.code();
                    String string = response.body().string();
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            textView.setText(code+"\n"+string);
                        }
                    });
                }
            });
        }
    }
);

// MyInteceptor.java
class MyInteceptor implements Interceptor{

    @Override
    public Response intercept( Chain chain) throws IOException {
        // 1. 请求头部设置缓存信息
        Request request = chain.request().newBuilder()
                .cacheControl(CacheControl.FORCE_CACHE)  // 强制从缓存中读取
                .build();

        // todo
        Log.e("TAG", "intercept: ");
        // 2. 响应请求头添加
        // 移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性
        return chain.proceed(request).newBuilder()
                .removeHeader("Pragma")
                .removeHeader("Cache-Control")
                .addHeader("Cache-Control", "max-age=1800")
                .build();
    }
}

注意添加权限。


References

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦否

文章对你有用?不妨打赏一毛两毛

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值