1 问题场景
一个网络请求接口,app启动时要请求一下,进入MainActivity时要请求一下,甚至每次onResume时都要请求一下。实际的情况可能不太一样,但是像这种一个接口要频繁请求多次的情况多少应该都有遇到,我们需要接口的数据,但是实际数据的变动又没有那么频繁。这样每一次都请求网络可能会造成网络资源浪费和等待。
2 问题的处理思路
2.1 不执行实际网络请求,也不返回数据
比如通过数据更新界面的功能,数据一样就没必要更新界面了。可以在某一个时间段直接不调用请求接口方法,以减少界面的无效刷新,达到限制请求间隔的效果
2.2 缓存请求数据结果
有缓存之后同一段时间内再有相同的请求直接返回缓存数据。
这一次本文要讲的是OkHttp自带缓存到磁盘的功能。
3 问题的解决
3.1 启用OkHttp缓存功能
3.1.1 OkHttp缓存相关的初始化配置
@Singleton
@Provides
fun provideOkHttpClient(app: Application): OkHttpClient {
// 10m
val diskCacheSize = 10L shl 20
return OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
//这里给OkHttp设置缓存功能
.cache(Cache(File(app.externalCacheDir, "net"), diskCacheSize))
.build()
这里OkHttpClient.Builder().cache(Cache(File(app.externalCacheDir, "net"), diskCacheSize))
给OkHttp设置缓存功能,但实际会不会触发缓存还得看返回数据的请求头配置。
3.1.2 手动给返回数据的请求头配置有效缓存时长
给OkHttp设置的拦截器
private fun Request.isCachePostRequest(): Boolean = run {
url.toString().contains(APP_INFO_URL, true)
class CachePostResponseInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
// 发起请求
var response = chain.proceed(request)
// 获得请求结果
// 为该接口设置缓存
if (response.request.isCachePostRequest()) {
response = response.newBuilder()
.removeHeader("Pragma")
// 缓存 60秒
.addHeader("Cache-Control", "max-age=60")
.build()
return response
给接口数据设置缓存的方法有好几种,这里通过配置请求头参数Cache-Control
的方法设置缓存,缓存时长60秒。
然后给OkHttp添加这个拦截器
@Singleton
@Provides
fun provideOkHttpClient(app: Application): OkHttpClient {
// 10m
val diskCacheSize = 10L shl 20
return OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
//这里给OkHttp设置缓存功能
.cache(Cache(File(app.externalCacheDir, "net"), diskCacheSize))
// 添加给返回数据设置缓存的拦截器
.addNetworkInterceptor(CachePostResponseInterceptor())
.build()
这里看样子应该大功告成,不过要验证是否缓存需要添加一下日志,还是通过拦截器的方法,这里临时添加一个用于打印的拦截器。
@Singleton
@Provides
fun provideOkHttpClient(app: Application): OkHttpClient {
// 10m
val diskCacheSize = 10L shl 20
return OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
//这里给OkHttp设置缓存功能
.cache(Cache(File(app.externalCacheDir, "net"), diskCacheSize))
// 临时的用于打印日志的拦截器
.addInterceptor {
val request = it.request()
val response = it.proceed(request)
Timber.e("cacheResponse: ${response.cacheResponse} networkResponse: ${response.networkResponse}")
response
// 添加给返回数据设置缓存的拦截器
.addNetworkInterceptor(CachePostResponseInterceptor())
.build()
这里为什么CachePostResponseInterceptor
拦截器是用addNetworkInterceptor
方法添加,而日志打印拦截器是通过addInterceptor
方法添加先不解释,要讲清楚原理需要讲解OkHttp拦截器的工作原理和责任链设计模式的工作流程,这些内容都可以另开几篇文章写了。
先运行看一下效果。
E/NetWorkModule$provideOkHttpClient$$inlined$-addInterceptor: (Interceptor.kt:78)
cacheResponse: null networkResponse: Response{protocol=http/1.1, code=200, message=, url=...}
E/NetWorkModule$provideOkHttpClient$$inlined$-addInterceptor: (Interceptor.kt:78)
cacheResponse: null networkResponse: Response{protocol=http/1.1, code=200, message=, url=...}
两次请求时间间隔60秒内,cacheResponse
都是null
,networkResponse
都有值。
这说明请求数据没有被缓存成功,正常的应该第一次请求cacheResponse
为null
,networkResponse
有值,第二次请求cacheResponse
有值,networkResponse
为null
。
为什么OkHttp
没有缓存我们的接口数据?我们去看一下OkHttp是怎么缓存数据的。
3.1.3 OkHttp缓存数据的工作逻辑
OkHttp
中缓存数据的工作是交给CacheInterceptor
拦截器
查看CacheInterceptor
类的代码可以发现缓存的保存是在网络请求数据返回时并且Cache
对象引用存在,这个Cache
就是前面设置OkHttpClient.Builder().cache(Cache(File(app.externalCacheDir, "net"), diskCacheSize))
时的Cache
。
val response = networkResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
if (cache != null) {
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
// 一个关键的点
// Offer this request to the cache.
val cacheRequest = cache.put(response)
return cacheWritingResponse(cacheRequest, response).also {
if (cacheResponse != null) {
// This will log a conditional cache miss only.
listener.cacheMiss(call)
再看看Cache
的put
方法
internal fun put(response: Response): CacheRequest? {
val requestMethod = response.request.method
if (HttpMethod.invalidatesCache(response.request.method)) {
try {
remove(response.request)
} catch (_: IOException) {
// The cache cannot be written.
return null
// 只支持 GET请求
if (requestMethod != "GET") {
// Don't cache non-GET responses. We're technically allowed to cache HEAD requests and some
// POST requests, but the complexity of doing so is high and the benefit is low.
return null
if (response.hasVaryAll()) {
return null
val entry = Entry(response)
var editor: DiskLruCache.Editor? = null
try {
editor = cache.edit(key(response.request.url)) ?: return null
entry.writeTo(editor)
return RealCacheRequest(editor)
} catch (_: IOException) {
abortQuietly(editor)
return null
可以看到只支持缓存GET
请求,不是GET
请求直接返回null
。
看看我们的请求接口,是一个POST
请求!
@POST(APP_INFO_URL)
suspend fun appInfo(@Body map: Map<String, String?>): Response<AppInfo>
怎么办怎么办,OkHttp
只有GET
请求才缓存数据,这是合理的,但是我们有时却碰到POST
请求需要缓存数据的情况,这样的一种情况存在可能说明后端写的接口请求方式不太合适,要后端去改吗?不是很有必要。
Cache
类又不能继承。
自己改,怎么改?有两个思路
复制OkHttp
处理缓存的Cache
类和CacheInterceptor
类,修改Cache
的put
方法支持缓存POST
请求,然后在复制的CacheInterceptor
类中把Cache
类的声明引用指向复制修改后的Cache
类的对象,将修改后的CacheInterceptor
类的对象添加到OkHttp
的拦截器列表中。
这是网上能搜到的做法,我觉得复制的代码过多,增加相似功能的类(原Cache
类和新Cache
类,原CacheInterceptor
类和新CacheInterceptor
类)。
在OkHttp
处理缓存拦截器工作前把特定需要缓存数据的POST
请求改成GET
,先通过缓存这一关(如果有有效的缓存数据直接返回缓存数据),然后在发出实际网络请求前还原为POST
请求去正确请求数据,等请求数据回来又重新把POST
请求改成GET
(以缓存数据)。
这个处理思路只需要两个拦截器,是我采取的处理方案。
3.2 让OkHttp缓存Post请求
3.2.1 在OkHttp
处理缓存拦截器工作前把特定需要缓存数据的POST
请求转成GET
请求
这个拦截器要解决的问题是告诉缓存拦截器CacheInterceptor
该接口数据可缓存,如果有有效的缓存数据会直接返回缓存数据。
* POST 转换成 GET
class TransformPostRequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
// 缓存
if (request.isCachePostRequest()) {
val builder = request.newBuilder()
// 把 POST 改成 GET
.method("GET", null)
.cacheControl(
CacheControl.Builder()
.maxAge(60, TimeUnit.SECONDS)
.build()
// 保存 body
saveRequestBody(builder, request.body)
request = builder.build()
return chain.proceed(request)
这个拦截器的关键核心是把POST
请求改成GET
请求,调用request.newBuilder().method("GET", request.body)
构造新的请求就是GET
请求,但是一运行你会发现程序Crash(崩溃)了
查看method
方法,发现对于GET
请求,他不让我们设置RequestBody
open fun method(method: String, body: RequestBody?): Builder = apply {
require(method.isNotEmpty()) {
"method.isEmpty() == true"
if (body == null) {
require(!HttpMethod.requiresRequestBody(method)) {
"method $method must have a request body."
} else {
require(HttpMethod.permitsRequestBody(method)) {
"method $method must not have a request body."
this.method = method
this.body = body
@kotlin.internal.InlineOnly
public inline fun require(value: Boolean, lazyMessage: () -> Any): Unit {
contract {
returns() implies value
if (!value) {
val message = lazyMessage()
throw IllegalArgumentException(message.toString())
@JvmStatic // Despite being 'internal', this method is called by popular 3rd party SDKs.
fun permitsRequestBody(method: String): Boolean = !(method == "GET" || method == "HEAD")
而这个RequestBody
是我们的请求参数信息,是必需要保存的,不然丢失请求参数。怎么办,只能反射给他设置。
private fun saveRequestBody(builder: Request.Builder, body: RequestBody?) {
val bodyField = builder.javaClass.getDeclaredField("body")
bodyField.isAccessible = true
bodyField.set(builder, body)
3.2.2 还原为POST
请求去发出实际请求,等请求数据回来又重新把POST
请求改成GET
以缓存数据
这个拦截器要处理的问题是要正确的发出网络请求,同时等网络请求数据回来要告诉缓存拦截器CacheInterceptor
该接口数据需要缓存起来。
* Response的缓存
class CachePostResponseInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
// 实际请求前
if (request.isCachePostRequest()) {
request = request.newBuilder()
.method("POST", request.body)
.build()
// 发起请求
var response = chain.proceed(request)
// 获得请求结果
// 为该接口缓存
if (response.request.isCachePostRequest()) {
val builder = response.request.newBuilder()
// 把 POST 改成 GET
.method("GET", null)
// 保存 body
saveRequestBody(builder, request.body)
response = response.newBuilder()
.request(builder.build())
.removeHeader("Pragma")
// 缓存 60秒
.addHeader("Cache-Control", "max-age=60")
.build()
return response
给OkHttp设置拦截器
@Singleton
@Provides
fun provideOkHttpClient(app: Application): OkHttpClient {
// 10m
val diskCacheSize = 10L shl 20
return OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
//这里给OkHttp设置缓存功能
.cache(Cache(File(app.externalCacheDir, "net"), diskCacheSize))
.addInterceptor(TransformPostRequestInterceptor())
.addInterceptor {
val request = it.request()
val response = it.proceed(request)
Timber.e("cacheResponse: ${response.cacheResponse} networkResponse: ${response.networkResponse}")
response
// 添加给返回数据设置缓存的拦截器
.addNetworkInterceptor(CachePostResponseInterceptor())
.build()
运行看看看效果。
E/NetWorkModule$provideOkHttpClient$$inlined$-addInterceptor: (Interceptor.kt:78)
cacheResponse: null networkResponse: Response{protocol=http/1.1, code=200, message=, url=...}
E/NetWorkModule$provideOkHttpClient$$inlined$-addInterceptor: (Interceptor.kt:78)
cacheResponse: Response{protocol=http/1.1, code=200, message=, url=...} networkResponse: null
两次请求时间间隔60秒内,第一次请求没有缓存数据,会发出实际网络请求,数据回来应该会被缓存起来。这时cacheResponse
为null
,networkResponse
有值。
第二次请求有缓存数据,直接返回缓存数据,不会再去发出实际网络请求。这时cacheResponse
有值,networkResponse
为null
。
实际的log打印符合我们的预期,接口数据被成功缓存和返回了。
最后的收尾总结
利用拦截器让OkHttp
缓存POST
请求的思路
第一步,在缓存拦截器工作前告诉缓存拦截器CacheInterceptor
该接口数据可缓存,如果有有效的缓存数据会直接返回缓存数据。
第二步,在发出实际网络请求前还原为POST
请求
第三步,等网络请求数据回来时告诉缓存拦截器CacheInterceptor
该接口数据需要缓存起来。
相关的代码已在文中全部贴出。