OKHttp使用Interceptor的缓存问题

178307940 8年前
   <h3>前言</h3>    <p>网上关于OKHttp的文章特别多,用法大家都知道,这里就只说说关于使用OKHttp中的拦截器来做缓存的问题。之前从来没有使用过拦截器做缓存,所以一开始也是从网上一顿搜,大部分文章都是差不多的,然后就把网上的代码拷贝过来了,不出所料,果然和文中的效果不一样,下面就说说这两天踩过的坑。</p>    <h3>概念</h3>    <p>对于拦截器的详细论述我也在网上搜索看了好多,这里给出我的理解: <strong>拦截器</strong> :简单来说就是在使用OKHttp访问网络的时候,可以通过自定义拦截器将发送的Request拦截下来,然后就可以做一些操作,比如添加一些请求头参数等。</p>    <p>那么我们怎么通过配置请求头或者响应头来达到缓存呢?</p>    <p>答案是OKHttp已经帮我们完成了,我们只需要按照它的要求去配置好请求头或者响应头就可以了,那么怎么配置呢?这里说一下,不管是web中浏览器的缓存还是Android中使用OKHttp来做缓存,都是通过响应头(即 Response 的 header )来完成的。拿Android使用OKHttp来说,客户端是拿到服务器的响应以后,OKHttp是通过获取响应中的参数来配置对应的缓存。那么请求头又有什么用呢?这就要分两种情况了。</p>    <p>一:可以和服务器协商</p>    <p>这种情况的时候,我们可以要求服务端来按照我们的要求来配置对应缓存的响应头参数,然后我们只负责解析就可以达到缓存的目的。这种情况,我们不需要为了缓存再写拦截器来做配置。</p>    <p>二:不能和服务器协商</p>    <p>很多时候,服务器根本不是我们自己维护的,所以这时就需要我们自己来完成缓存的响应头设置了,那我们怎么拿到服务器返回的响应然后添加自己响应头呢?就是通过拦截器,使用拦截器不仅可以拦截发出的 Request 请求,同时还可以配置收到的 Response 响应,配置好之后返回就可以了</p>    <h3>使用</h3>    <p>先说一个依赖的问题,一般现在我们的网络库都是Rxjava+ Retrofit + OKHttp,由于Retrofit 中内置了OKHttp,所以我们在依赖的时候,只依赖Retrofit就可以了,不要再去依赖OKHttp,因为我在网上看到有人因为两个的版本问题出现了各种奇怪的问题,这样浪费精力就得不偿失了。下面是我的依赖</p>    <pre>  <code class="language-java">//Rxjava,Rxandroid      compile 'io.reactivex.rxjava2:rxandroid:2.0.1'      compile 'io.reactivex.rxjava2:rxjava:2.0.1'      //RxJava2 Adapter      compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'      //retrofit      compile 'com.squareup.retrofit2:retrofit:2.1.0'      //Gson converter      compile 'com.squareup.retrofit2:converter-gson:2.1.0'</code></pre>    <p>接下来开始上代码,然后在说说我遇到的坑和注意事项。</p>    <p>既然要做缓存,就先说缓存策略:</p>    <p>分为有网络和无网络的情况,在有网络的时候,先读取缓存中的内容,缓存时间到之后访问网络拿数据,在没有网络的时候读取缓存数据。</p>    <p>下面贴出判断网络代码</p>    <pre>  <code class="language-java">/**       * 判断是否有网络       *       * @return 返回值       */      public static boolean isNetworkConnected() {          Context context = RxApplication.getContext();          if (context != null) {              ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);              NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();                if (mNetworkInfo != null) {                  return mNetworkInfo.isAvailable();              }          }          return false;      }</code></pre>    <p>别忘了加权限</p>    <pre>  <code class="language-java"><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>      <uses-permission android:name="android.permission.INTERNET"/></code></pre>    <p>定义一个拦截器</p>    <pre>  <code class="language-java">public class MyCacheInterceptor implements Interceptor {        @Override      public Response intercept(Chain chain) throws IOException {          return null;      }</code></pre>    <p>只要实现 Interceptor 接口,实现一个方法就可以。可以看到接口返回的是一个 Response 对象,我们就可以把配置好的响应头直接返回就可以了,怎么配置呢?看下面。</p>    <p>然后是在有网络的时候需要在拦截器中添加的设置</p>    <pre>  <code class="language-java">@Override      public Response intercept(Chain chain) throws IOException {          //拦截Request对象          Request request = chain.request();          //判断有无网络连接          boolean connected = isNetworkConnected();         if (connected) {              //有网络,缓存时间短,缓存90s              String cacheControl = request.cacheControl().toString();             //这里返回的就是我们获取到的响应头,添加缓存配置返回              return response.newBuilder()                      .removeHeader("Pragma")                      .header("Cache-Control","public, max-age=90")                      .build();          }</code></pre>    <p>这里就有个疑惑了:看上面的代码,在这个拦截器中我们既可以获得 Request 请求对象,同时返回的竟然是服务器的 Response 响应头对象,那么拦截器在什么时机拦截的请求呢?又在什么时机拦截的响应呢?这个我没有看源码,只能通过效果反推一下,通过在有网的时候打印Log可以发现,当访问网络的时候,Log打印有时两次有时多次,我推测,一个拦截器在客户端发送到服务端的线路上会拦截一次,在服务端发送回客户端的线路上又会拦截一次,这样也就做到了,既可以拦截 Request 请求,也做到了拦截服务器返回的 Response 响应。当然我只是怎么推测,有知道的朋友请留言告诉我一些(感谢)。总之它的作用就是这样的。</p>    <p>然后是在没有网络的时候添加的设置</p>    <pre>  <code class="language-java">if (!connected) {            //没有网络时设置强制读取缓存              int maxTime = 3600;              return response.newBuilder()                      //这里的设置的是我们的没有网络的缓存时间,想设置多少就是多少。                      .header("Cache-Control", "public, only-if-cached, max-age=" + maxTime)                      .removeHeader("Pragma")                      .build();          }</code></pre>    <p>最后贴出拦截器的完整代码</p>    <pre>  <code class="language-java">public class MyCacheInterceptor implements Interceptor {      @Override      public Response intercept(Chain chain) throws IOException {          //拦截Request对象          Request request = chain.request();          //判断有无网络连接          boolean connected = isNetworkConnected();          if (!connected) {              //如果没有网络,从缓存获取数据              request = request.newBuilder()                      .cacheControl(CacheControl.FORCE_CACHE)                      .build();            Log.e("zhanghe", "no network");          }          Response response = chain.proceed(request);            if (connected) {              //有网络,缓存时间短              Log.e("zhanghe", "有网络");              String cacheControl = request.cacheControl().toString();              return response.newBuilder()                      .removeHeader("Pragma")                      .header("Cache-Control","public, max-age=90")                      .build();          } else {              //没有网络              Log.e("zhanghe", "没有网络的缓存设置");              int maxTime = 3600;              return response.newBuilder()                      //这里的设置的是我们的没有网络的缓存时间,想设置多少就是多少。                      .header("Cache-Control", "public, max-age=" + maxTime)                      .removeHeader("Pragma")                      .build();          }      }  }</code></pre>    <p>然后还要创建一个缓存路径再将这个拦截器添加到Client中</p>    <pre>  <code class="language-java">File file = new File(RxApplication.getContext().getCacheDir(), "rxCache");    //缓存大小10M  int cacheSize = 10 * 1024 * 1024;  Cache cache = new Cache(file, cacheSize);  OkHttpClient client = new OkHttpClient.Builder()                  .cache(cache)                        //设置缓存                                 .addNetworkInterceptor(cacheInterceptor)//添加拦截器                               .connectTimeout(5, TimeUnit.SECONDS) //连接超时                  .writeTimeout(5, TimeUnit.SECONDS)   //写入超时                  .readTimeout(5, TimeUnit.SECONDS)  //读取超时                  .build();</code></pre>    <p>然后就可以构造Retrofit对象了</p>    <pre>  <code class="language-java">Retrofit mRetrofit = new Retrofit.Builder()                  .client(client)                  .baseUrl(BASE_URL)                  .addConverterFactory(GsonConverterFactory.create())                  .addCallAdapterFactory(RxJava2CallAdapterFactory.create())                  .build();</code></pre>    <p>之后的操作我就不说了,这不是文章的重点。</p>    <h3>坑一</h3>    <p>代码写完了,网上的例子都是这么写的,然后我就去运行了,但是,我怎么知道OKHttp什么时候在访问网络,什么时候在访问缓存呢?所以我想到了使用Fiddler来看,然后就有了代码中那些Log输出,然后开始调试,解释结果是每次Fiddler都会显示从网络上下载数据了,而且按照常理,从服务器获取到的响应头会有添加的 Cache-Control 属性,但是没有。</p>    <p>先告诉大家吧,在调试的时候千万千万千万不要用Fiddler,否则你会被坑惨的,最后我自己写了一个接口,第一次访问完之后,马上把接口中的数据换了这样来测试是不是读取的缓存,结果证明Fiddler也还是会显示出一条http请求 (可能是我不会用Fiddler吧,不过还是建议大家自己写个接口来测比较方便)。</p>    <p>对于响应头的 Cache-Control 属性,Fiddler确实是显示不出来,但是通过代码获取到的响应头确实添加了,获取响应头代码如下:</p>    <pre>  <code class="language-java">new Thread(new Runnable() {              @Override              public void run() {                  try {                      Call<MyInfo> call = RetrofitManager.getInstance()                              .createApi(MovieApi.class)                             .getInfo();                      Response<MyInfo> execute = call.execute();                      //获取响应头                      Headers headers = execute.raw().headers();                      //获取响应吗                      int code = execute.code();                      //获取对应的字段                      String s = headers.get("Cache-Control");                      Log.e("zhang", s);                  } catch (IOException e) {                      e.printStackTrace();                  }              }          }).start();</code></pre>    <p>最终结论:Fiddler很坑,调试的时候不要用它。</p>    <h3>坑二</h3>    <p>我在调试中,在有网的情况下都可以可以的,即第一次从网络上获取之后,然后在访问就是走的缓存,但是在将网络断了之后,总是不能读取缓存数据.Google,百度了好久,网上差不多都是类似上面的代码,好多还是就直接转载的,不知道有没有测试过就发表出来了,好坑(呜呜呜。。。)</p>    <p>然后我就找到lygttpod的GitHub邮箱,邮件问他,发现自己上面代码少调用一个方法,就是在添加拦截器方法的位置,代码再贴一遍</p>    <pre>  <code class="language-java">File file = new File(RxApplication.getContext().getCacheDir(), "rxCache");    //缓存大小10M  int cacheSize = 10 * 1024 * 1024;  Cache cache = new Cache(file, cacheSize);  OkHttpClient client = new OkHttpClient.Builder()                  .cache(cache)                        //设置缓存                  .addInterceptor(cacheInterceptor)            //☆☆☆                                 .addNetworkInterceptor(cacheInterceptor)//添加拦截器                               .connectTimeout(5, TimeUnit.SECONDS) //连接超时                  .writeTimeout(5, TimeUnit.SECONDS)   //写入超时                  .readTimeout(5, TimeUnit.SECONDS)  //读取超时                  .build();</code></pre>    <p>这里添加拦截器有两个方法 addInterceptor 和 addNetworkInterceptor ,分别表示添加作为应用拦截器和网络拦截器,想看着两种拦截器的区别点这里,其实我也不太明白两者的区别,但是在后边说缓存坑的时候会说到两者的区别。</p>    <p>添加了这行代码之后果然好用了,但是为什么呢?如果只添加一个会怎么样呢?</p>    <p>至于为什么要添加两个,我没有去看源码所以无法解答,如果有朋友知道,请留言告诉我以上,但是我测试了分别添加一个的结果:</p>    <p>一:只添加网络拦截器</p>    <p>在有网络的时候,缓存逻辑是正常的,第一次访问网络,缓存到本地,之后会先去读缓存,设置的缓存时间到了以后,会去访问网络;但是断网的时候,OKHttp就不会去读缓存了,为什么呢?debug之后,发现是这行代码出现了异常: Response response = chain.proceed(request); 抛出的异常为: java.net.SocketException: sendto failed: ETIMEDOUT (Connection timed out) 但是异常不会导致程序正常逻辑出现错误,之后导致断网状态不会读取缓存</p>    <p>二:只添加应用拦截器</p>    <p>在有网络的时候,OKHttp每次都会去访问网络,不会去读缓存;但是在断网的时候,代码正常运行,因为上面代码配置了缓存逻辑,所以OKHttp会去读缓存。</p>    <p>看了上面的测试结果,就明白了为什么要将那一个拦截器对象add两次,因为只有add两次才能够满足我们的缓存逻辑:在有网络的时候,先去读缓存,缓存时间到了,再去访问网络获取数据;在没有网络的时候,去读缓存中的数据。</p>    <p>但是add两次也有一个弊端:就是因为同一个拦截器add了两次,在有网的情况下,是有缓存就读缓存的,读缓存的时候理论上是不应该再走拦截器的,但因为add了两次,所以每次都会再走一遍拦截器,测试的时候每次读缓存都会打印这句话 Log.e("zhanghe", "有网络");</p>    <h3>最后</h3>    <p>最后我感觉既然是两种情况加的拦截器,即有网情况和无网情况,所以我就建立了两个对应的类,经过测试也避免了上面说的弊端。代码如下:</p>    <pre>  <code class="language-java">/**   *   * 在有网络的情况下,先去读缓存,设置的缓存时间到了,在去网络获取   */    public class NetInterceptor implements Interceptor {      @Override      public Response intercept(Chain chain) throws IOException {          Request request = chain.request();          boolean connected = NetUtil.isNetworkConnected();          if(connected){              //如果有网络,缓存90s              Log.e("zhanghe","print");              Response response = chain.proceed(request);              int maxTime = 90;              return response.newBuilder()                      .removeHeader("Pragma")                      .header("Cache-Control", "public, max-age=" + maxTime)                      .build();          }          //如果没有网络,不做处理,直接返回          return chain.proceed(request);           }  }</code></pre>    <pre>  <code class="language-java">/**   * author: zh on 2017/4/13.   * 在没有网络的情况下,取读缓存数据   */    public class NoNetInterceptor implements Interceptor {      @Override      public Response intercept(Chain chain) throws IOException {            Request request = chain.request();          boolean connected = NetUtil.isNetworkConnected();          //如果没有网络,则启用 FORCE_CACHE          if (!connected) {              request = request.newBuilder()                      .cacheControl(CacheControl.FORCE_CACHE)                      .build();              Log.e("zhanghe", "无网络设置_common");                Response response = chain.proceed(request);              return response.newBuilder()                      .header("Cache-Control", "public, only-if-cached, max-stale=3600")                      .removeHeader("Pragma")                      .build();          }          //有网络的时候,这个拦截器不做处理,直接返回          return chain.proceed(request);      }  }</code></pre>    <p>添加到 OkHttpClient 的时候就可以分别添加</p>    <pre>  <code class="language-java">OkHttpClient client = new OkHttpClient.Builder()                    .cache(cache)                  .addInterceptor(new NoNetInterceptor())    //将无网络拦截器当做应用拦截器添加                  .addNetworkInterceptor(new NetInterceptor()) //将有网络拦截器当做网络拦截器添加                  //.addInterceptor(cacheInterceptor)                  //.addNetworkInterceptor(cacheInterceptor)                  .connectTimeout(DUFAULT_TIME_OUT, TimeUnit.SECONDS) //连接超时                  .writeTimeout(DUFAULT_TIME_OUT, TimeUnit.SECONDS)   //写入超时                  .readTimeout(DUFAULT_TIME_OUT, TimeUnit.SECONDS)  //读取超时                  .build();</code></pre>    <p>上面就是我对OKHttp的拦截器做缓存的理解,哪里有不对的地方请留言,或者github上邮箱发给我</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/cf59500990c7</p>    <p> </p>