OKHttp3(支持Retrofit)的网络数据缓存Interceptor拦截器

前言:前段时间在开发APP的时候,经常出现由于用户设备环境的原因,拿不到从网络端获取的数据,所以在APP端展现的结果总是一个空白的框,这种情况对于用户体验来讲是极其糟糕的,所以,苦思冥想决定对OKHTTP下手(因为我在项目中使用的网络请求框架就是OKHTTP),则 写了这么一个网络数据缓存拦截器。

OK,那么我们决定开始写了,我先说一下思路:

思路篇

既然要写的是网络数据缓存拦截器,主要是利用了OKHTTP强大的拦截器功能,那么我们应该对哪些数据进行缓存呢,或者在哪些情况下启用数据进行缓存机制呢?

  • 第一 :支持POST请求,因为官方已经提供了一个缓存拦截器,但是有一个缺点,就是只能对GET请求的数据进行缓存,对POST则不支持。
  • 第二 :网络正常的时候,则是去网络端取数据,如果网络异常,比如TimeOutException UnKnowHostException 诸如此类的问题,那么我们就需要去缓存取出数据返回。
  • 第三 :如果从缓存中取出的数据是空的,那么我们还是需要让这次请求走剩下的正常的流程。
  • 第四 :调用者必须对缓存机制完全掌控,可以根据自己的业务需求选择性的对数据决定是否进行缓存。
  • 第五 :使用必须简单,这是最最最最重要的一点。

好,我们上面罗列了五点是我们的大概思路,现在来说一下代码部分:

代码篇

  • 缓存框架 :我这里使用的缓存框架是DiskLruCache https://github.com/JakeWharton/DiskLruCache 这个缓存框架可以存储到本地,也经过谷歌认可,这也是选择这个框架的主要原因。我这里也对缓存框架进行封装了一个CacheManager类:
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;

import com.xiaolei.OkhttpCacheInterceptor.Log.Log;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


/**
 * Created by xiaolei on 2017/5/17.
 */

public class CacheManager
{
    public static final String TAG = "CacheManager";

    //max cache size 10mb
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 10;

    private static final int DISK_CACHE_INDEX = 0;

    private static final String CACHE_DIR = "responses";

    private DiskLruCache mDiskLruCache;

    private volatile static CacheManager mCacheManager;

    public static CacheManager getInstance(Context context)
    {
        if (mCacheManager == null)
        {
            synchronized (CacheManager.class)
            {
                if (mCacheManager == null)
                {
                    mCacheManager = new CacheManager(context);
                }
            }
        }
        return mCacheManager;
    }

    private CacheManager(Context context)
    {
        File diskCacheDir = getDiskCacheDir(context, CACHE_DIR);
        if (!diskCacheDir.exists())
        {
            boolean b = diskCacheDir.mkdirs();
            Log.d(TAG, "!diskCacheDir.exists() --- diskCacheDir.mkdirs()=" + b);
        }
        if (diskCacheDir.getUsableSpace() > DISK_CACHE_SIZE)
        {
            try
            {
                mDiskLruCache = DiskLruCache.open(diskCacheDir,
                        getAppVersion(context), 1/*一个key对应多少个文件*/, DISK_CACHE_SIZE);
                Log.d(TAG, "mDiskLruCache created");
            } catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }

    /**
     * 同步设置缓存
     */
    public void putCache(String key, String value)
    {
        if (mDiskLruCache == null) return;
        OutputStream os = null;
        try
        {
            DiskLruCache.Editor editor = mDiskLruCache.edit(encryptMD5(key));
            os = editor.newOutputStream(DISK_CACHE_INDEX);
            os.write(value.getBytes());
            os.flush();
            editor.commit();
            mDiskLruCache.flush();
        } catch (IOException e)
        {
            e.printStackTrace();
        } finally
        {
            if (os != null)
            {
                try
                {
                    os.close();
                } catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 异步设置缓存
     */
    public void setCache(final String key, final String value)
    {
        new Thread()
        {
            @Override
            public void run()
            {
                putCache(key, value);
            }
        }.start();
    }

    /**
     * 同步获取缓存
     */
    public String getCache(String key)
    {
        if (mDiskLruCache == null)
        {
            return null;
        }
        FileInputStream fis = null;
        ByteArrayOutputStream bos = null;
        try
        {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(encryptMD5(key));
            if (snapshot != null)
            {
                fis = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
                bos = new ByteArrayOutputStream();
                byte[] buf = new byte[1024];
                int len;
                while ((len = fis.read(buf)) != -1)
                {
                    bos.write(buf, 0, len);
                }
                byte[] data = bos.toByteArray();
                return new String(data);
            }
        } catch (IOException e)
        {
            e.printStackTrace();
        } finally
        {
            if (fis != null)
            {
                try
                {
                    fis.close();
                } catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
            if (bos != null)
            {
                try
                {
                    bos.close();
                } catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 异步获取缓存
     */
    public void getCache(final String key, final CacheCallback callback)
    {
        new Thread()
        {
            @Override
            public void run()
            {
                String cache = getCache(key);
                callback.onGetCache(cache);
            }
        }.start();
    }

    /**
     * 移除缓存
     */
    public boolean removeCache(String key)
    {
        if (mDiskLruCache != null)
        {
            try
            {
                return mDiskLruCache.remove(encryptMD5(key));
            } catch (IOException e)
            {
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * 获取缓存目录
     */
    private File getDiskCacheDir(Context context, String uniqueName)
    {
        String cachePath = context.getCacheDir().getPath();
        return new File(cachePath + File.separator + uniqueName);
    }

    /**
     * 对字符串进行MD5编码
     */
    public static String encryptMD5(String string)
    {
        try
        {
            byte[] hash = MessageDigest.getInstance("MD5").digest(
                    string.getBytes("UTF-8"));
            StringBuilder hex = new StringBuilder(hash.length * 2);
            for (byte b : hash)
            {
                if ((b & 0xFF) < 0x10)
                {
                    hex.append("0");
                }
                hex.append(Integer.toHexString(b & 0xFF));
            }
            return hex.toString();
        } catch (NoSuchAlgorithmException | UnsupportedEncodingException e)
        {
            e.printStackTrace();
        }
        return string;
    }

    /**
     * 获取APP版本号
     */
    private int getAppVersion(Context context)
    {
        PackageManager pm = context.getPackageManager();
        try
        {
            PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
            return pi == null ? 0 : pi.versionCode;
        } catch (PackageManager.NameNotFoundException e)
        {
            e.printStackTrace();
        }
        return 0;
    }
}
  • 缓存CacheInterceptor拦截器 :利用OkHttp的Interceptor拦截器机制,智能判断缓存场景,以及网络情况,对不同的场景进行处理。
import android.content.Context;

import com.xiaolei.OkhttpCacheInterceptor.Catch.CacheManager;
import com.xiaolei.OkhttpCacheInterceptor.Log.Log;

import java.io.IOException;

import okhttp3.FormBody;
import okhttp3.Interceptor;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

/**
 * 字符串的缓存类
 * Created by xiaolei on 2017/12/9.
 */
public class CacheInterceptor implements Interceptor
{
    private Context context;

    public void setContext(Context context)
    {
        this.context = context;
    }

    public CacheInterceptor(Context context)
    {
        this.context = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException
    {
        Request request = chain.request();
        String cacheHead = request.header("cache");
        String cache_control = request.header("Cache-Control");

        if ("true".equals(cacheHead) ||                              // 意思是要缓存
                (cache_control != null && !cache_control.isEmpty())) // 这里还支持WEB端协议的缓存头
        {
            long oldnow = System.currentTimeMillis();
            String url = request.url().url().toString();
            String responStr = null;
            String reqBodyStr = getPostParams(request);
            try
            {
                Response response = chain.proceed(request);
                if (response.isSuccessful()) // 只有在网络请求返回成功之后,才进行缓存处理,否则,404存进缓存,岂不笑话
                {
                    ResponseBody responseBody = response.body();
                    if (responseBody != null)
                    {
                        responStr = responseBody.string();
                        if (responStr == null)
                        {
                            responStr = "";
                        }
                        CacheManager.getInstance(context).setCache(CacheManager.encryptMD5(url + reqBodyStr), responStr);//存缓存,以链接+参数进行MD5编码为KEY存
                        Log.i("HttpRetrofit", "--> Push Cache:" + url + " :Success");
                    }
                    return getOnlineResponse(response, responStr);
                } else
                {
                    return chain.proceed(request);
                }
            } catch (Exception e)
            {
                Response response = getCacheResponse(request, oldnow); // 发生异常了,我这里就开始去缓存,但是有可能没有缓存,那么久需要丢给下一轮处理了
                if (response == null)
                {
                    return chain.proceed(request);//丢给下一轮处理
                } else
                {
                    return response;
                }
            }
        } else
        {
            return chain.proceed(request);
        }
    }

    private Response getCacheResponse(Request request, long oldNow)
    {
        Log.i("HttpRetrofit", "--> Try to Get Cache   --------");
        String url = request.url().url().toString();
        String params = getPostParams(request);
        String cacheStr = CacheManager.getInstance(context).getCache(CacheManager.encryptMD5(url + params));//取缓存,以链接+参数进行MD5编码为KEY取
        if (cacheStr == null)
        {
            Log.i("HttpRetrofit", "<-- Get Cache Failure ---------");
            return null;
        }
        Response response = new Response.Builder()
                .code(200)
                .body(ResponseBody.create(null, cacheStr))
                .request(request)
                .message("OK")
                .protocol(Protocol.HTTP_1_0)
                .build();
        long useTime = System.currentTimeMillis() - oldNow;
        Log.i("HttpRetrofit", "<-- Get Cache: " + response.code() + " " + response.message() + " " + url + " (" + useTime + "ms)");
        Log.i("HttpRetrofit", cacheStr + "");
        return response;
    }

    private Response getOnlineResponse(Response response, String body)
    {
        ResponseBody responseBody = response.body();
        return new Response.Builder()
                .code(response.code())
                .body(ResponseBody.create(responseBody == null ? null : responseBody.contentType(), body))
                .request(response.request())
                .message(response.message())
                .protocol(response.protocol())
                .build();
    }

    /**
     * 获取在Post方式下。向服务器发送的参数
     *
     * @param request
     * @return
     */
    private String getPostParams(Request request)
    {
        String reqBodyStr = "";
        String method = request.method();
        if ("POST".equals(method)) // 如果是Post,则尽可能解析每个参数
        {
            StringBuilder sb = new StringBuilder();
            if (request.body() instanceof FormBody)
            {
                FormBody body = (FormBody) request.body();
                if (body != null)
                {
                    for (int i = 0; i < body.size(); i++)
                    {
                        sb.append(body.encodedName(i)).append("=").append(body.encodedValue(i)).append(",");
                    }
                    sb.delete(sb.length() - 1, sb.length());
                }
                reqBodyStr = sb.toString();
                sb.delete(0, sb.length());
            }
        }
        return reqBodyStr;
    }

}

以上是主体思路,以及主要实现代码,现在来说一下使用方式

使用方式:

gradle使用:

compile 'com.xiaolei:OkhttpCacheInterceptor:1.0.0'

由于是刚刚提交到Jcenter,可能会出现拉不下来的情况(暂时还未过审核),着急的读者可以再在你的Project:build.gradle里的repositories里新增我maven的链接:

allprojects {
    repositories {
        maven{url 'https://dl.bintray.com/kavipyouxiang/maven'}
    }
}

我们新建一个项目,项目截图是这样的:

项目截图

demo很简单,一个主页面,一个Bean,一个Retrofit,一个网络请求接口

注意,因为是网络,缓存,有关,所以,毫无疑问我们要在manifest里面添加网络请求权限,文件读写权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

使用的时候,你只需要为你的OKHttpClient添加一个Interceptor:

client = new OkHttpClient.Builder()
                .addInterceptor(new CacheInterceptor(context))//添加缓存拦截器,添加缓存的支持
                .retryOnConnectionFailure(true)//失败重连
                .connectTimeout(30, TimeUnit.SECONDS)//网络请求超时时间单位为秒
                .build();

如果你想哪个接口的数据缓存,那么久为你的网络接口,添加一个请求头CacheHeaders.java这个类里包含了所有的情况,一般情况下只需要CacheHeaders.NORMAL就可以了

public interface Net
{
    @Headers(CacheHeaders.NORMAL) // 这里是关键
    @FormUrlEncoded
    @POST("geocoding")
    public Call<DataBean> getIndex(@Field("a") String a);
}

业务代码:

Net net = retrofitBase.getRetrofit().create(Net.class);
        Call<DataBean> call = net.getIndex("苏州市");
        call.enqueue(new Callback<DataBean>()
        {
            @Override
            public void onResponse(Call<DataBean> call, Response<DataBean> response)
            {
                DataBean data = response.body();
                Date date = new Date();
                textview.setText(date.getMinutes() + " " + date.getSeconds() + ":\n" + data + "");
            }

            @Override
            public void onFailure(Call<DataBean> call, Throwable t)
            {
                textview.setText("请求失败!");
            }
        });

我们这里对网络请求,成功了,则在界面上输出文字,加上当前时间,网络失败,则输出一个请求失败。

大概代码就是这样子的,详细代码,文章末尾将贴出demo地址

看效果:演示图

演示图

这里演示了,从网络正常,到网络不正常,再恢复到正常的情况。

结尾

以上篇章就是整个从思路,到代码,再到效果图的流程,这里贴一下DEMO的地址,喜欢的可以点个Start

Demo地址:https://github.com/xiaolei123/OkhttpCacheInterceptor

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏向治洪

深入理解React Native页面构建渲染原理

前言 React Native 是最近非常火的一个话题,因为它的语法简介,跨平台等特性,赢得了各大平台的青睐,虽然前期是有一些坑。 基本概念解释 React 是...

2769
来自专栏Java与Android技术栈

使用Kotlin高效地开发Android App(四)总结

在Kotlin的世界里,我们可以重载算数运算符,包括一元运算符、二元运算符和复合赋值运算符。

813
来自专栏冰霜之地

Weex 中别具匠心的 JS Framework

为了达到所有页面在用户端达到秒开,也就是网络(JS Bundle下载)和首屏渲染(展现在用户第一屏的渲染时间)时间和小于1s。

1003
来自专栏极客慕白的成长之路

jQuery深入——动画、常用工具、JSON、Ajax

4、停止动画 - stop([stopAll [, goToEnd]]) stopAll 布尔值,规定是否停止被选元素的所有加入队列的动画。默认是 false。...

971
来自专栏分布式系统进阶

Kafka中的时间轮Kafka源码分析-汇总

将TimerTask对象绑定到 TimerTaskEntry上 如果这个TimerTask对象之前已经绑定到了一个 TimerTaskEntry上, 先调用t...

721
来自专栏yukong的小专栏

【ssm个人博客项目实战05】easy ui datagrid实现数据的分页显示1、数据格式准备工作2、业务层实现3、控制层实现4、前端视图处理

前面一节 我们已经实现博客类别的dao层的实现,其中特别讲解了博客类别的分页的实现,那么现在我们实现了后台的分页,那么前台分页怎么显示呢,这时候我们用到了eas...

1022
来自专栏大内老A

一句代码实现批量数据绑定[上篇]

对于一个以数据处理为主的应用中的UI层,我们往往需要编写相当多的代码去实现数据绑定。如果界面上的控件和作为数据源的实体类型之间存储某种约定的映射关系,我们就可以...

1856
来自专栏郭霖

Android Volley完全解析(三),定制自己的Request

经过前面两篇文章的学习,我们已经掌握了Volley各种Request的使用方法,包括StringRequest、JsonRequest、ImageRequest...

1976
来自专栏积累沉淀

JavaScript事件

JavaScript事件 对于事件来讲,首先,我们需要了解这样几个概念:事件;事件处理程序;事件类型;事件流;事件冒泡;事件捕获;事件对象;事件模拟,事件方面的...

2076
来自专栏debugeeker的专栏

用xerces-c来进行xml schema校验

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xuzhina/article/detai...

401

扫码关注云+社区