美文网首页Android TechAndroid知识Android开发
给Retrofit添加离线缓存,支持Post请求

给Retrofit添加离线缓存,支持Post请求

作者: Wang_Yi | 来源:发表于2016-12-04 15:28 被阅读4251次

    需要实现的需求:

    有网络的时候使用网络获取数据,网络不可用的情况下使用本地缓存。
    Retrofit本身并没有可以设置缓存的api,它的底层网络请求使用Okhttp,所以添加缓存也得从Okhttp入手。

    一.Okhttp自带的缓存支持:

    首先设置缓存目录,Okhttp的缓存用到了DiskLruCache这个类。

            OkHttpClient.Builder builder = new OkHttpClient.Builder();
            File cacheDir = new File(context.getCacheDir(), "response");
            //缓存的最大尺寸10m
            Cache cache = new Cache(cacheDir, 1024 * 1024 * 10);
            builder.cache(cache);
    

    Okhttp缓存拦截器:

    public class CacheInterceptor implements Interceptor {
    
        @Override
        public okhttp3.Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            boolean netAvailable = NetWorkUtil.isNetAvailable(AppIml.appContext);
    
            if (netAvailable) {
                request = request.newBuilder()
                        //网络可用 强制从网络获取数据
                        .cacheControl(CacheControl.FORCE_NETWORK)
                        .build();
            } else {
                request = request.newBuilder()
                        //网络不可用 从缓存获取
                        .cacheControl(CacheControl.FORCE_CACHE)
                        .build();
            }
            Response response = chain.proceed(request);
            if (netAvailable) {
                response = response.newBuilder()
                        .removeHeader("Pragma")
                        // 有网络时 设置缓存超时时间1个小时
                        .header("Cache-Control", "public, max-age=" + 60 * 60)
                        .build();
            } else {
                response = response.newBuilder()
                        .removeHeader("Pragma")
                        // 无网络时,设置超时为1周
                        .header("Cache-Control", "public, only-if-cached, max-stale=" + 7 * 24 * 60 * 60)
                        .build();
            }
            return response;
        }
    }
    

    给OkHttpClient 设置拦截器,并用我们创建的OkHttpClient 替代Retrofit 默认的OkHttpClient:

        builder.addInterceptor(new CacheInterceptor());
    
        OkHttpClient client = builder.build();
        Retrofit retrofit = new Retrofit.Builder()
                .client(client)
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    

    到这里Okhttp的缓存就配置完成了,实现了开头所提出的需求。
    但是这里有个问题,Okhttp是只支持Get请求的,如果我们使用其他方式请求比如Post,请求的能够回调onResponse方法,但是通过 response.body()来获取请求的数据会得到null, response.code()得到的是504。
    我还目前没有找到能够让Okhttp的缓存支持Post方式的方法,所以我只能自己去实现缓存机制。

    二.自己手动添加缓存支持:

    首先将 DiskLruCache.java 添加进来,我们和Okhttp一样使用它来实现磁盘缓存策略。
    关于DiskLruCache的源码分析:Android DiskLruCache完全解析,硬盘缓存的最佳方案
    写一个工具类 来设置和获取缓存:

    public final 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() {
            if (mCacheManager == null) {
                synchronized (CacheManager.class) {
                    if (mCacheManager == null) {
                        mCacheManager = new CacheManager();
                    }
                }
            }
            return mCacheManager;
        }
    
        private CacheManager() {
            File diskCacheDir = getDiskCacheDir(AppIml.appContext, 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(AppIml.appContext), 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;
        }
    }
    

    然后我们还是给Okhttp添加拦截器,将请求的Request和请求结果Response以Key Value的形式缓存的磁盘。这里的重点是判断请求的方式,如果是Post请求这将请求的body转成String然后添加到url的后面作为磁盘缓存的key。

    public class EnhancedCacheInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Response response = chain.proceed(request);
    
            String url = request.url().toString();
            RequestBody requestBody = request.body();
            Charset charset = Charset.forName("UTF-8");
            StringBuilder sb = new StringBuilder();
            sb.append(url);
            if (request.method().equals("POST")) {
                MediaType contentType = requestBody.contentType();
                if (contentType != null) {
                    charset = contentType.charset(Charset.forName("UTF-8"));
                }
                Buffer buffer = new Buffer();
                try {
                    requestBody.writeTo(buffer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                sb.append(buffer.readString(charset));
                buffer.close();
            }
            Log.d(CacheManager.TAG, "EnhancedCacheInterceptor -> key:" + sb.toString());
    
            ResponseBody responseBody = response.body();
            MediaType contentType = responseBody.contentType();
    
            BufferedSource source = responseBody.source();
            source.request(Long.MAX_VALUE);
            Buffer buffer = source.buffer();
    
            if (contentType != null) {
                charset = contentType.charset(Charset.forName("UTF-8"));
            }
            String key = sb.toString();
            //服务器返回的json原始数据 
            String json = buffer.clone().readString(charset);
    
            CacheManager.getInstance().putCache(key, json);
            Log.d(CacheManager.TAG, "put cache-> key:" + key + "-> json:" + json);
            return chain.proceed(request);
        }
    }
    

    创建OkHttpClient并添加缓存拦截器,初始化Retrofit;

            OkHttpClient.Builder builder = new OkHttpClient.Builder();
            builder.addInterceptor(new EnhancedCacheInterceptor());
    
            OkHttpClient client = builder.build();
            Retrofit retrofit = new Retrofit.Builder()
                    .client(client)
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
    

    到这里我们已经实现了请求网络添加缓存,下一步是在网络不可用的时候获取磁盘上的缓存,这里我通过改造Call和Callback来实现:
    EnhancedCall使用装饰模式装饰Retrofit的Call,EnhancedCallback比Retrofit的Callback接口多一个onGetCache方法。
    网络不可用的时候会回调onFailure方法,我们拦截onFailure,并根据请求Request去获取缓存,获取到缓存走EnhancedCallback的onGetCache方法,如果没有获取到缓存或者请求不需要使用缓存再调用onFailure方法,

    public class EnhancedCall<T> {
        private Call<T> mCall;
        private Class dataClassName;
        // 是否使用缓存 默认开启
        private boolean mUseCache = true;
    
        public EnhancedCall(Call<T> call) {
            this.mCall = call;
        }
    
        /**
         * Gson反序列化缓存时 需要获取到泛型的class类型
         */
        public EnhancedCall<T> dataClassName(Class className) {
            dataClassName = className;
            return this;
        }
    
        /**
         * 是否使用缓存 默认使用
         */
        public EnhancedCall<T> useCache(boolean useCache) {
            mUseCache = useCache;
            return this;
        }
    
        public void enqueue(final EnhancedCallback<T> enhancedCallback) {
            mCall.enqueue(new Callback<T>() {
                @Override
                public void onResponse(Call<T> call, Response<T> response) {
                    enhancedCallback.onResponse(call, response);
                }
    
                @Override
                public void onFailure(Call<T> call, Throwable t) {
                    if (!mUseCache || NetWorkUtil.isNetAvailable(AppIml.appContext)) {
                        //不使用缓存 或者网络可用 的情况下直接回调onFailure
                        enhancedCallback.onFailure(call, t);
                        return;
                    }
    
                    Request request = call.request();
                    String url = request.url().toString();
                    RequestBody requestBody = request.body();
                    Charset charset = Charset.forName("UTF-8");
                    StringBuilder sb = new StringBuilder();
                    sb.append(url);
                    if (request.method().equals("POST")) {
                        MediaType contentType = requestBody.contentType();
                        if (contentType != null) {
                            charset = contentType.charset(Charset.forName("UTF-8"));
                        }
                        Buffer buffer = new Buffer();
                        try {
                            requestBody.writeTo(buffer);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        sb.append(buffer.readString(charset));
                        buffer.close();
                    }
    
                    String cache = CacheManager.getInstance().getCache(sb.toString());
                    Log.d(CacheManager.TAG, "get cache->" + cache);
    
                    if (!TextUtils.isEmpty(cache) && dataClassName != null) {
                        Object obj = new Gson().fromJson(cache, dataClassName);
                        if (obj != null) {
                            enhancedCallback.onGetCache((T) obj);
                            return;
                        }
                    }
                    enhancedCallback.onFailure(call, t);
                    Log.d(CacheManager.TAG, "onFailure->" + t.getMessage());
                }
            });
        }
    }
    
    public interface EnhancedCallback<T> {
        void onResponse(Call<T> call, Response<T> response);
    
        void onFailure(Call<T> call, Throwable t);
    
        void onGetCache(T t);
    }
    

    到这里已经实现了最开始的我的需求,也可以支持Post请求的缓存。最后看看使用的方式:

        public void getRequest(View view) {
            ApiService service = getApiService();
            Call<UserList> call = service.getUserList();
            //使用我们自己的EnhancedCall 替换Retrofit的Call
            EnhancedCall<UserList> enhancedCall = new EnhancedCall<>(call);
            enhancedCall.useCache(true)/*默认支持缓存 可不设置*/
                    .dataClassName(UserList.class)
                    .enqueue(new EnhancedCallback<UserList>() {
                        @Override
                        public void onResponse(Call<UserList> call, Response<UserList> response) {
                            UserList userlist = response.body();
                            if (userlist != null) {
                                Log.d(TAG, "onResponse->" + userlist.toString());
                            }
                        }
    
                        @Override
                        public void onFailure(Call<UserList> call, Throwable t) {
                            Log.d(TAG, "onFailure->" + t.getMessage());
                        }
    
                        @Override
                        public void onGetCache(UserList userlist) {
                            Log.d(TAG, "onGetCache" + userlist.toString());
                        }
                    });
        }
    

    全部代码地址Github:https://github.com/wangyiwy/CacheUtil4Retrofit)

    相关文章

      网友评论

      • Moustar:写的挺好的,但是有个bug,希望修改 return chain.proceed(request); 会导致2次重复请求,解决return response ;
      • d112479fa89c:我怎么知道我获取得数据是否过时呢?
      • 飞天舞乐:谢谢楼主分享,缓存这块我做了封装,使用起来更方便一些,欢迎大家拍砖讨论:
        https://github.com/yale8848/RetrofitCache
        那个唐僧:你这个我看了,只有get没有post
      • 纯洁的坏蛋:有配合Rxjava的嘛:smile:
      • 无辜的小黄人:有配合Rxjava的嘛:smile:
        越努力越幸运阳:其实楼主这2步可以合成1个Interceptor就可以做到了,在Interceptor 判断没有网络就获取缓存,这样就和用不用rxjava就没有区别了。
      • 光羽隼:怎样像get请求那样设置有网的时候读取缓存啊
      • 仙桃:有个疑问,缓存不都是针对GET请求吗,POST请求修改服务器数据,没网络怎么修改。。难道是Api写的不规范,改用GET的用了POST?:smile:
        善笃有余劫:@大海螺Utopia token 都是header头吧
        仙桃:@大海螺Utopia 可以放到header里,通常放到Authorization中
        大海螺Utopia:是的,我们公司就有这种情况,有的时候传一个token参数去拉取数据,但是又不希望token通过get拼在url里,就这么搞了
      • 沈敏杰:通俗易懂,喜欢!
      • woitaylor:确实写的不错。okhttp那个拦截器真的只是对get请求有效吗?我不是很清楚。
      • Xingye_:质量很高

      本文标题:给Retrofit添加离线缓存,支持Post请求

      本文链接:https://www.haomeiwen.com/subject/qcehmttx.html