添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

1 问题场景

一个网络请求接口,app启动时要请求一下,进入MainActivity时要请求一下,甚至每次onResume时都要请求一下。实际的情况可能不太一样,但是像这种一个接口要频繁请求多次的情况多少应该都有遇到,我们需要接口的数据,但是实际数据的变动又没有那么频繁。这样每一次都请求网络可能会造成网络资源浪费和等待。

2 问题的处理思路

2.1 不执行实际网络请求,也不返回数据

比如通过数据更新界面的功能,数据一样就没必要更新界面了。可以在某一个时间段直接不调用请求接口方法,以减少界面的无效刷新,达到限制请求间隔的效果

2.2 缓存请求数据结果

  • 缓存到内存(变量或LRU)
  • 缓存到数据库
  • 缓存到磁盘
  • 有缓存之后同一段时间内再有相同的请求直接返回缓存数据。

    这一次本文要讲的是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都是nullnetworkResponse都有值。

    这说明请求数据没有被缓存成功,正常的应该第一次请求cacheResponsenullnetworkResponse有值,第二次请求cacheResponse有值,networkResponsenull

    为什么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)

    再看看Cacheput方法

    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类,修改Cacheput方法支持缓存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秒内,第一次请求没有缓存数据,会发出实际网络请求,数据回来应该会被缓存起来。这时cacheResponsenullnetworkResponse有值。

    第二次请求有缓存数据,直接返回缓存数据,不会再去发出实际网络请求。这时cacheResponse有值,networkResponsenull

    实际的log打印符合我们的预期,接口数据被成功缓存和返回了。

    最后的收尾总结

    利用拦截器让OkHttp缓存POST请求的思路

    第一步,在缓存拦截器工作前告诉缓存拦截器CacheInterceptor该接口数据可缓存,如果有有效的缓存数据会直接返回缓存数据。

    第二步,在发出实际网络请求前还原为POST请求

    第三步,等网络请求数据回来时告诉缓存拦截器CacheInterceptor该接口数据需要缓存起来。

    相关的代码已在文中全部贴出。

    分类:
    Android
  •