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

QQ群:559259945

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,098评论 1 23
  • 1 到底多久才能养成一个习惯呢? 看过很多习惯快速养成秘诀,可是依旧养不成一个好...
    一只甜包阅读 550评论 6 5
  • 抱歉,是我陷入了迷雾 不知道是对还是错 前进也好 后退也罢 不值钱的是承诺 拥有也好 失去也罢 不过是不合适的相爱...
    行走的亡魂阅读 167评论 0 1
  • 梭罗其实从来没有真正按《瓦尔登湖》宣扬的那样在树林里孤独而自立地生活过,他几乎每天都有访客,如果没人来看他,他就会...
    小妇阿达阅读 314评论 0 2