实现PHP服务器+Android客户端(Retrofit+RxJava)第三天Retrofit的配置以及缓存的实现

来源:互联网 发布:天池大数据竞赛 案例 编辑:程序博客网 时间:2024/06/06 05:42

上一篇讲了界面,这篇文章就要讲客户端网络请求部分的内容了,主要用到的就是Retrofit+RxJava,其实准确来说是Retrofit+RxJava+OkHttp,
最新的Retrofit是2.0.2版本,源码地址:retrofit
学习retrofit:用 Retrofit 2 简化 HTTP 请求
大家不要觉得使用那么多的框架好像不太好,对于个人开发者来说,一个人懂的东西还是有限的,要自己做一个完整的项目还是需要借助一些框架,而且我觉得还是先把想要做的东西做出来再说,先会用这些框架再去学习看源码也不迟,在使用的过程中就已经能收获一些,而且也帮助后续理解。
接下来的内容就来说说Retrofit+RxJava+OkHttp的简单配置
参考了以下文章:Retrofit2.0使用总结
Retrofit 2.0 + OkHttp 3.0 配置

依赖

compile 'com.squareup.retrofit2:retrofit:2.0.2'compile 'com.squareup.retrofit2:converter-gson:2.0.2'compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'compile 'com.squareup.okhttp3:okhttp:3.0.1'

OkHttp的配置

HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);OkHttpClient client = new OkHttpClient.Builder()        .addInterceptor(interceptor)        .retryOnConnectionFailure(true)        .connectTimeout(15, TimeUnit.SECONDS)        .addNetworkInterceptor(mTokenInterceptor)        .build();
  • HttpLoggingInterceptor 是一个拦截器,用于输出请求和响应的log,可以配置 level 为 BASIC / HEADERS / BODY
  • retryOnConnectionFailure就是出现错误时是否重试
  • connectTimeout超时时间
  • addNetworkInterceptor让所有网络请求都附上你的拦截器,我这里设置了一个 token 拦截器,就是在所有网络请求的 header 加上 token 参数。
    这里可能会对拦截器有些疑问,拦截器一般的作用也就是重写请求报文的头部信息,内容或者重写响应报文的头部信息(非正规做法,可以由服务器完成,比如做缓存)。拦截器分为以下两种
  • 应用拦截器 addInterceptor()
  • 网络拦截器 addNetworkInterceptor()
    对于这两个的区分我也还没有到位,大致做个区分:应用拦截器即使请求的是缓存也会调用,而网络拦截器不会。应用拦截器在重定向的时候只调用一次,网络拦截器会调用两次。一般使用的话要打印log就在应用拦截器就行,而要添加什么头部啊就用网络拦截器。
    可自行参考:Okhttp-wiki 之 Interceptors 拦截器

Retrofit配置

Retrofit retrofit = new Retrofit.Builder()        .baseUrl(BASE_URL)        .client(client)        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())        .addConverterFactory(GsonConverterFactory.create(gson))        .build();        apiService = retrofit.create(ApiService.class);

缓存的实现

首先要实现缓存就要先确定缓存的目录,这个目录需要在app卸载之后也被删除,所以一般就两个目录

  • data/$packageName/cache: Context.getCacheDir()
  • /storage/sdcard0/Andorid/data/$packageName/cache: Context.getExternalCacheDir()

其余代码如下

private static final int HTTP_RESPONSE_DISK_CACHE_MAX_SIZE = 10 * 1024 * 1024;  private Cache cache() {         //设置缓存路径         final File baseDir = AppUtil.getAvailableCacheDir(sContext);         final File cacheDir = new File(baseDir, "HttpResponseCache");         //设置缓存 10M         return new Cache(cacheDir, HTTP_RESPONSE_DISK_CACHE_MAX_SIZE);     }

不要忘记:

OkHttpClient client = new OkHttpClient.Builder()        .addInterceptor(interceptor)        .retryOnConnectionFailure(true)        .connectTimeout(15, TimeUnit.SECONDS)        .addNetworkInterceptor(mTokenInterceptor)        .cache(chche())        .build();

上面的工作只是第一步,接下来才是真正缓存的实现,缓存实现我看到的有三种方式

第一种

有网和没网都先读缓存,统一缓存策略。
根据响应报文的Cache-Control字段或者直接修改响应报文的Cache-Control字段做缓存(其实控制缓存的不止这一个字段,我这里就只讲这一个,方便)。
先来说说http协议吧,首先我们客户端发送一个http请求报文(不包含Cache-Control字段),随后服务器返回http响应报文,然后我们可以根据响应报文中的Cache-Control字段来决定是否进行缓存,缓存在多长时间内有效等,假设返回的Cache-Control字段内容的如下:

Cache-Control: max-age=60, public

意思是60秒内有效,60s内再次请求此地址就直接拿缓存,60s之后就再去请求服务器,大致流程如下图:
这里写图片描述
这一种方式主要是靠服务器来做缓存策略,不过,加入我们的服务器没有返回缓存相关的字段如Cache-Control: max-age=60, public,我们也可以用拦截器自己在响应报文中加上,用的拦截器是网络拦截器(忘了网络拦截器的可以回头看看),拦截器代码如下:

Interceptor cacheInterceptor = new Interceptor() {            @Override            public Response intercept(Chain chain) throws IOException {                Request request = chain.request();                Response response = chain.proceed(request);                return response.newBuilder()                        .header("Cache-Control", "max-age=60")                        .removeHeader("Pragma")                        .build();            }        };

连着自己的服务器测试之后,第一次请求在相应目录下创建了缓存,在我上面实现的拦截器中打印log输出的响应首部中也添加了Cache-Control字段,随后再次请求相同的路径就没有再输出log信息(和我们上面说的网络拦截器不会在请求读取缓存的时候调用相吻合)。
这一种方式只要知道最基本的http协议的知识就能很快理解。

第二种

第二种方式如下(但其实是有问题的,接下来讨论):

// 设置 单个请求的 缓存时间@Headers("Cache-Control: max-age=640000")@GET("widget/list")Call<List<Widget>> widgetList();

在请求的首部加上Cache-Control字段,最后再去看缓存目录下面也生成了缓存。就我那时候知道的http知识来理解我很困惑,不是Cache-Control字段只作用在响应报文中吗?为什么在请求报文的首部添加缓存控制也能实现响应报文的缓存。后来再去看了看http权威指南,请求报文中也能有Cache-Control字段是看到了,但是一些含义还是看的迷迷糊糊,于是决定还是自己来做实验好了。
以下测试针对请求报文的首部Cache-Control字段:

Cache-Control:no-cache,如何测试呢,需要借助第一种方式在本地生成一次缓存,随后发送带有Cache-Control:no-cache的请求,你会发现网络拦截器在输出响应体,再看缓存中的Date那一行信息发现一直在变,说明向,服务器请求了数据,重新写了缓存。由此可得请求报文中 Cache-Control:no-cache的意思是会被缓存的,只不过每次在向客户端(浏览器)提供响应数据时,缓存都要向服务器评估缓存响应的有效性。
Cache-Control:max-stale是只在请求报文中有效的,首先网络拦截器添加的响应报文的首部是Cache-Control: max-age=60, public,也就是缓存在60s内有效,随后设置请求的Cache-Control:max-stale=120.照着htpp协议的意思应该是120s之内都从缓存取数据,但是实际却输出了响应报文的body,也就是说进行了网络请求,再看请求报文的首部信息,其中带着If-Modified-since信息,说明是缓存过期了,但是我明明很快就再次请求了,再把max-stale=120时间改大一点,改成1200,再测试,没有打印log信息,说明符合我理解的http的语义,响应报文设置的缓存有效时间是60s,请求报文max-stale设置的是1200s有效,过了60s依旧从缓存中取。但是这个时间有问题!!!暂时先不管这个,把http的语义搞清楚再说。
上面的情况是在响应报文有缓存策略的情况下,接下来看看没有在响应报文中说明缓存策略的情况,先把网络拦截器给响应报文添加Cache-Control字段的代码去掉,测试结果生成了缓存(只要完成了第一步设置了cache(chche())就会默认缓存),在过期时间内也从缓存中读取。
Cache-Control:max-age这个字段意思我开始不太明白,后来结合响应首部的Cache-Control:max-age联合使用的时候突然明白了,假设只在请求首部添加Cache-Control:max-age,你会发现创建了缓存,但是立马就去请求了服务器(我猜这个时候失效时间为0,因为没有设置响应首部的Cache-Control:max-age),单独设置响应首部的Cache-Control:max-age那是有用的,就是我们的第一种方式。如果请求和响应的首部都设置了,就取小的那一个。
是不会返回缓存时间超过这个时间的文档,和响应首部的Cache-Control:max-age联合使用的时候,响应首部的Cache-Control:max-age,失效时间取小的那一个
结合上述,简单总结起来可以说Cache-Control:max-age规定的是失效时间的下限制(如果请求首部没有写默认是很大,如果响应首部没有写那就是0,最终的max-age取两者的小值),Cache-Control:max-stale规定的是上限(只在请求首部有效)。
看到这里大家应该也知道了,像上面

// 设置 单个请求的 缓存时间@Headers("Cache-Control: max-age=640000")@GET("widget/list")Call<List<Widget>> widgetList();

这样是会创建缓存,但是请求不会走缓存,需要配合拦截器设置响应报文中的Cache-Control。
拦截器代码如下:

Interceptor cacheInterceptor = new Interceptor() {            @Override            public Response intercept(Chain chain) throws IOException {                Request request = chain.request();                Response response = chain.proceed(request);                String cacheControl = request.cacheControl().toString();                if (TextUtils.isEmpty(cacheControl)) {                    return response;                }                return response.newBuilder()                        .header("Cache-Control", cacheControl )                        .removeHeader("Pragma")                        .build();            }        };

第三种

离线读取本地缓存,在线获取最新数据

private Interceptor cacheInterceptor() {        return new Interceptor() {            @Override            public Response intercept(Chain chain) throws IOException {                Request request = chain.request();                if (!AppUtil.isNetworkReachable(sContext)) {                    request = request.newBuilder()                            //强制使用缓存                            .cacheControl(CacheControl.FORCE_CACHE)                            .build();                }                Response response = chain.proceed(request);                if (AppUtil.isNetworkReachable(sContext)) {                    //有网的时候读接口上的@Headers里的配置,你可以在这里进行统一的设置                    String cacheControl = request.cacheControl().toString();                    Logger.i("has network ,cacheControl=" + cacheControl);                    return response.newBuilder()                            .header("Cache-Control", cacheControl)                            .removeHeader("Pragma")                            .build();                } else {                    int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale                    Logger.i("network error ,maxStale="+maxStale);                    return response.newBuilder()                            .header("Cache-Control", "public, only-if-cached, max-stale="+maxStale)                            .removeHeader("Pragma")                            .build();                }            }        };    }

大家看到上面的代码可能会有疑问,有疑问的地方如下:

 else {                    int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale                    Logger.i("network error ,maxStale="+maxStale);                    return response.newBuilder()                            .header("Cache-Control", "public, only-if-cached, max-stale="+maxStale)                            .removeHeader("Pragma")                            .build();                }

这里的代码其实都是没有用的,都可以去掉,only-if-cached和max-stale都只在请求报文中有效,真正做到,没有网络的时候很长时间缓存有效是在这几句代码:

if (!AppUtil.isNetworkReachable(sContext)) {                    request = request.newBuilder()                            //强制使用缓存                            .cacheControl(CacheControl.FORCE_CACHE)                            .build();                }

所以上面的代码可以改为如下:

private Interceptor cacheInterceptor() {        return new Interceptor() {            @Override            public Response intercept(Chain chain) throws IOException {                Request request = chain.request();                if (!AppUtil.isNetworkReachable(sContext)) {                    request = request.newBuilder()                            //强制使用缓存                            .cacheControl(CacheControl.FORCE_CACHE)                            .build();                }                Response response = chain.proceed(request);                if (AppUtil.isNetworkReachable(sContext)) {                    //有网的时候读接口上的@Headers里的配置,你可以在这里进行统一的设置                    String cacheControl = request.cacheControl().toString();                    Logger.i("has network ,cacheControl=" + cacheControl);                    return response.newBuilder()                            .header("Cache-Control", cacheControl)                            .removeHeader("Pragma")                            .build();                }                 return response;            }        };    }

参考:OkHttp3源码分析[缓存策略]
使用Retrofit和Okhttp实现网络缓存。无网读缓存,有网根据过期时间重新请求

0 0