zoukankan      html  css  js  c++  java
  • 一些你可能需要的okhttp实现

    https://blog.csdn.net/qq_17766199/article/details/53186874

    今天分享一些我在项目中使用到的okhttp实现,由简至难。(以下内容均在okhttp3.4.1下正常使用)

    1.okhttp日志打印

    这个就简单了,一个工具类。先上代码:

    public class LoggingInterceptor implements Interceptor {
    
        @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    
            Request request = chain.request();
            KLog.d(String.format("Sending request %s on %s%n%s", request.url(),  chain.connection(), request.headers()));
    
            long t1 = System.nanoTime();
            okhttp3.Response response = chain.proceed(chain.request());
            long t2 = System.nanoTime();
            KLog.d(String.format(Locale.getDefault(), "Received response for %s in %.1fms%n%s",
                    response.request().url(), (t2 - t1) / 1e6d, response.headers()));
    
            okhttp3.MediaType mediaType = response.body().contentType();
            String content = response.body().string();
            KLog.json(content);
            return response.newBuilder()
                    .body(okhttp3.ResponseBody.create(mediaType, content))
                    .build();
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这里我为了打印清晰使用了KLog用来打印Log。

    使用方法:

    OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .addInterceptor(new LogInterceptor())
                    .build();
    • 1
    • 2
    • 3

    打印例子: 
    这里写图片描述

    当然了,你也可以使用官方提供的Logging Interceptor,也是非常方便。具体参见链接

    2.okhttp网络缓存

    (1)首先设置Cache

    private static File cacheFile = new File(context.getCacheDir(), "Test");
    private static Cache cache = new Cache(cacheFile, 1024 * 1024 * 10);
    private static OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .cache(cache)
                .build();
    • 1
    • 2
    • 3
    • 4
    • 5

    这里使用getCacheDir()来作为缓存文件的存放路径(/data/data/包名/cache) ,如果你想看到缓存文件可以临时使用 getExternalCacheDir()(/sdcard/Android/data/包名/cache)。

    (2)如果我们的服务器支持缓存,那么Response中的头文件中会有Cache-Control: max-age=xxx这样的字段。如下图:

    这里写图片描述

    这里的public意思是可以无条件的缓存该响应,max-age是你缓存的最大存放的时间。比如你设置了6分钟的最大缓存时间,那么6分钟内他会读取缓存,但超过这个时间则缓存失效。具体的响应标头大家可以自行查询。

    (3)如果我们的服务器不支持缓存,也就是响应头没有对应字段,那么我们可以使用网络拦截器实现:

    public class CacheInterceptor implements Interceptor {
    
        @Override public Response intercept(Chain chain) throws IOException {
    
            Request request = chain.request();
            if (!isNetworkConnected()) {
                request = request.newBuilder()
                        .cacheControl(CacheControl.FORCE_CACHE)
                        .build();
            }
            Response originalResponse = chain.proceed(request);
            if (isNetworkConnected()) {
                //有网的时候读接口上的@Headers里的配置,你可以在这里进行统一的设置(注掉部分)
                String cacheControl = request.cacheControl().toString();
                return originalResponse.newBuilder()
                        .header("Cache-Control", cacheControl)
                        //.header("Cache-Control", "max-age=3600")
                        .removeHeader("Pragma") // 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
                        .build();
            } else {
                int maxAge= 60 * 60;
                return originalResponse.newBuilder()
                        .header("Cache-Control", "public, only-if-cached, max-age=" + maxAge)
                        .removeHeader("Pragma")
                        .build();
            }
        }
    
        private boolean isNetworkConnected() {
            ConnectivityManager connectivity = (ConnectivityManager) MyApplication.getInstance()
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            if (null != connectivity){
                NetworkInfo info = connectivity.getActiveNetworkInfo();
                if (null != info && info.isConnected()){
                    if (info.getState() == NetworkInfo.State.CONNECTED){
                        return true;
                    }
                }
            }
            return false;
        }
    
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    设置拦截器:

     private static OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .cache(cache)
                .addInterceptor(new CacheInterceptor())
                .addNetworkInterceptor(new CacheInterceptor())
                .build();
    • 1
    • 2
    • 3
    • 4
    • 5

    注意:addInterceptoraddNetworkInterceptor 需要同时设置。 这两者的区别可以参考Interceptors 拦截器。我只说一下效果,如果你只是想实现在线缓存,那么可以只添加网络拦截器,如果只想实现离线缓存,可以使用只添加应用拦截器。两者都添加,就不用我说了吧。

    如果在拦截器中统一配置,则所有的请求都会缓存。但是在实际开发中有些接口需要保证数据的实时性,那么我们就不能统一配置,这时可以这样:

     @Headers("Cache-Control: public, max-age=时间秒数")
     @GET("weilu/test")
     Observable<Test> getData();
    • 1
    • 2
    • 3

    我自己找了一些配置,大家可以根据个人需求使用:

    1.不需要缓存:Cache-Control: no-cacheCache-Control: max-age=0

    2.如果想先显示数据,在请求。(类似于微博等):Cache-Control: only-if-cached

    通过以上配置后通过拦截器中的request.cacheControl().toString() 就可以获取到我们配置的Cache-Control头文件,实现对应的缓存策略。

    测试一下:

    1.首先我设置@Headers("Cache-Control: public,max-age=30")

    2.30秒有效期内请求第一次

    这里写图片描述

    3.30内请求第二次(在线缓存)

    这里写图片描述

    你可能会说没有什么不同,其实仔细看看就会发现,两次的请求响应时间分别为85.5ms和1.5ms,这说明是直接读取的缓存。同时我们可以查看Monitors中的Network发现并没有请求网络,也同样说明使用的是缓存。

    从这里其实也就说明了添加缓存的好处:1.降低了请求的延迟。2.降低网络的频繁请求。

    3.30超出后再请求一次结果与第二步一致。

    这里写图片描述

    缓存失效,重新请求。

    4.关闭网络后超过30s请求一次(离线缓存)

    这里写图片描述

    这时发现警告响应失效。这说明已经超过30秒,如果没有超出则没有警告。大家可以自行尝试。

    最后感兴趣的可以去我们设置的缓存目录查看一下缓存文件,你一定会有新的发现。

    3.okhttp实现token过期刷新

    (1)使用拦截器实现,这个具体参看这篇文章,我自己没有试过,提供出来给大家拓展一下思路。

    (2)使用RxJava的操作符retryWhen 实现。这个是我在项目中使用的方法,也是重点说明的方法。

    首先说明一下我们这边的情况,我们的登录验证使用的是登陆成功后包含在响应头文件中的Set-Cookie,这其中有sessionId,凡是需要登录操作的只需请求时带上他即可。

    登录超时时服务器会返回code为401的json来告知我们登录超时。那么其实就很简单了,只需要判断code是否为401,是则登录一下保存新的Cookie,在重新请求一下之前的操作就行了。

    下来我来实现以下,至于其他的情况其实大同小异。

    首先我们来说明一下retryWhenretryWhen操作符是在源Observable出现错误或者异常时,通过回调另一个Observable来判断是否重新尝试执行源Observable的逻辑,如果这个Observable没有错误或者异常出现,则就会重新尝试执行源Observable的逻辑,否则就会直接回调执行订阅者的onError方法。

    看了上面的概念是不是有点绕,其实意思就和他的名字一样“重试”,而触发的条件是有了“错误”,错误解决,那么再试一次。其实这就和我们需要解决的问题一样,登录失败(触发)–> 重新登录(解决) –> 重试。

    首先定义一个父类。

    public class TimeOut {
        private int timeout;
        public int getTimeout() {
            return timeout;
        }
        public void setTimeout(int timeout) {
            this.timeout = timeout;
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    自定义一个异常,可以不用实现

    public class TimeOutException extends Exception{
    }
    • 1
    • 2

    简单封装了一下,这样不用重复书写超时判断。

    protected Subscription subscription;
    
    public <B extends TimeOut> void toSubscribe(Observable<B> o, Subscriber<B> s) {
            unSubscribe();
            subscription = o.subscribeOn(Schedulers.io())
                    .unsubscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .flatMap(new Func1<B, Observable<B>>() {
                        @Override
                        public Observable<B> call(B timeout) {
                            if(timeout.getTimeout() == 401){
                                return Observable.error(new TimeOutException()); //超时,则触发retryWhen
                            }
                            return Observable.just(timeout);
                        }
                    })
                    .retryWhen(new TimeOutRetry()) //<----
                    .subscribe(s);
        }
    
    public void unSubscribe(){
            if (subscription != null)
                subscription.unsubscribe();
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    TimeOutRetry 类:

    public class TimeOutRetry implements Func1<Observable<? extends Throwable>, Observable<?>>{
    
        @Override
        public Observable<?> call(Observable<? extends Throwable> observable) {
            return observable.flatMap(new Func1<Throwable, Observable<?>>() {
                @Override
                public Observable<?> call(Throwable throwable) {
    
                    if (throwable instanceof TimeOutException) {
                        KLog.e("-----重登进入-----");
                        String name = 获取用户名;
                        String pwd = 获取密码;
    
                        if(TextUtils.isEmpty(name) || TextUtils.isEmpty(pwd)){
                            KLog.e("--- 超时重登未完成 ---");
                            return Observable.error(throwable);
                        }
                        return mApi().login(name, pwd)
                                .subscribeOn(Schedulers.io())
                                .observeOn(AndroidSchedulers.mainThread())
                                .unsubscribeOn(Schedulers.io())
                                .doOnNext(new Action1<Login>() {
                                    @Override
                                    public void call(Login login) {
                                        KLog.e("--- 登录完成 ---");
                                    }
                                });
                    }
                    return Observable.error(throwable); // 其他异常直接结束
                }
            });
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    使用:

     public void test(){
    
            toSubscribe(mApi.getData(), new Subscriber<Test>() {
                @Override
                public void onCompleted() {
    
                }
    
                @Override
                public void onError(Throwable e) {
    
                }
    
                @Override
                public void onNext(Test test) {
    
                }
            });
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    注意这里的Test类需要继承TimeOut。

    结果如图:

    这里写图片描述

    这里写图片描述

    看完这么多,我们可以看到这些功能都可以利用拦截器去实现,可见它的强大之处。(补充:说到了拦截器,突然想到了Facebook出的一个强大的Android调试工具stetho,该工具让你可以在谷歌浏览器查看App的布局,preference,网络请求,数据库,一切都是可视化的操作,不需要root你的设备。其中也支持okhttp,使用方法就是添加一个网络拦截器。)还有一些实现,比如https的访问,cookie的同步问题我就不一一去说了,我们可以参考okhttputilsokhttp-OkGo 去实现。好了,本篇到此结束!如果文章中有错误的地方希望指出,多多交流。最后喜欢的点个赞哈!

    4.参考

    1. Interceptors

    2. 使用Retrofit和Okhttp实现网络缓存

    3. Okhttp缓存浅析

    4. Recipes

    5. Rxjava+Retrofit 实现全局过期 token 自动刷新


    ps:发现文章中图片看的不是很清晰,大家可以右键–>打开图片,来查看大图。

    扫码向博主提问

    qq_17766199

    非学,无以致疑;非问,无以广识
    • 擅长领域:
    • Android
    • 单元测试
    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_17766199/article/details/53186874
  • 相关阅读:
    第二章 java内存区域与内存溢出异常
    TCP实现可靠传输
    Tomcat的类加载架构
    浅析Synchronized
    设计数据库
    http和https
    IOC容器的依赖注入
    IOC容器初始化
    深入理解Java内存模型
    单例应该这么写
  • 原文地址:https://www.cnblogs.com/tc310/p/9101804.html
Copyright © 2011-2022 走看看