Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 手動緩存Retrofit+OkHttp響應體

手動緩存Retrofit+OkHttp響應體

編輯:關於Android編程

概括

在上一篇博客中僅僅是簡單的講解了OkHttp的緩存問題,主要是通過http協議裡面的control-cache控制緩存,而且是僅僅只能是Get請求才能緩存,如果Post請求OkHttp會讓response返回null,同時報504錯誤,也就是沒緩存。okhttp為什麼要這樣做呢?通過查看緩存的文件,我們可以發現,OkHttp緩存的是整個http請求的信息,所以這就和http協議有關系了。在RESTful API裡面,我們把Get請求理解為從服務端查詢數據,Post請求理解為更新服務端數據,而http協議裡面緩存通常只適用於idempotent request,也就是Get請求,為什麼只適應Get請求?我們都知道Get請求url結合提交參數是唯一標示,而Post請求的參數是在http的body體裡面,是可變的,無法成為唯一的標示。但是,我們在項目中基本上每一個接口都要提交基本參數,一般用的都是Post請求。Get請求還不太安全,請求的路徑大小還有限制。既然OkHttp有限制。那麼我們可以自己手動緩存。

android的緩存處理

既然要手動緩存,那麼我們就要來看看android裡面手動緩存有哪些。主要有兩種方式,一種是sqlite緩存,一種是文件緩存。

sqlite緩存
目前有很多第三方sqlite框架,比如可以結合GreenDao來做緩存,一個緩存對應一個表。把url路經,下載時間,過期時間等信息都存放到數據庫。然後把url做為請求的唯一標示,在有網的情況下,判斷當前請求url緩存是否存在,存在就要移除數據庫裡面的緩存,然後緩存新的緩存,在沒有網絡的情況下,判斷緩存是否過期,然後進行數據庫操作。從這裡我們可以看出,數據庫操作還是比較頻繁的,一不留神,就會出現應用性能問題,ANR問題,指針問題。而且android數據庫是放在/data/data/<包名>/databases/目錄下,它會占用應用內存的,一但緩存很多的話,就要及時去清理緩存,很麻煩。

文件緩存
為什麼說文件緩存更好呢?如果SD存在的話,我們可以把緩存放在SD的/data/data/<包名>/cache目錄下,不存在SD的話,再放在/data/data/<包名>下面。即使內存再多,也不會影響應用的內置應用空間。文件緩存一般都會通過DiskLruCache實現,DiskLruCache是硬盤緩存,即使應用進程結束了,緩存還是存在的。當應用卸載時,改目錄的數據也會清除掉,不會留下殘余數據。DiskLruCache緩存,沒有什麼過期時間之說,只要它存在文件裡面,我們就可以隨時去讀取它。下面我們就用DiskLruCache對Retrofit+OkHttp的響應體進行緩存。這裡我們只緩存json數據。

DiskLruCache的使用方法

獲取DiskLruCache對象

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

不能直接通過new的方法創建,要通過調用DiskLruCache.open()這個方法獲取,有四個參數,File指的是緩存的存儲路徑,一般優先存儲於SD卡的 /sdcard/Android/data/<包名>/cache 路徑下,如果SD卡不存在,再存在/data/data/<包名>/cache 這個路徑下,判斷代碼如下

   private File getDiskCacheDir(Context context, String uniqueName)
    {
        String cachePath;

        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable())
        {
            //如果SD卡存在通過getExternalCacheDir()獲取路徑,
            cachePath = context.getExternalCacheDir().getPath();
        } else
        { 
            //如果SD卡不存在通過getCacheDir()獲取路徑,
            cachePath = context.getCacheDir().getPath();
        }
        //放在路徑 /.../data//cache/uniqueName
        return new File(cachePath + File.separator + uniqueName);
    }

appVersion指的是版本號,可以指應用的版本號,valueCount指的就是一個key對應多少個文件,一般我們指定1個文件,一對一使得後面更好獲取。maxSize指的是緩存的最大大小,一般傳入5M或者10M就夠了。

寫入緩存

首先我們先獲取一個DiskLruCache.Editor對象,代碼如下

    public DiskLruCache.Editor editor(String key)
    {
        try
        {
            key = Utils.hashKeyForDisk(key);
            //wirte DIRTY
            DiskLruCache.Editor edit = mDiskLruCache.edit(key);
            //edit maybe null :the entry is editing
            if (edit == null)
            {
                Log.w(TAG, "the entry spcified key:" + key + " is editing by other . ");
            }
            return edit;
        } catch (IOException e)
        {
            e.printStackTrace();
        }

        return null;
    }

首先進行的是Utils.hashKeyForDisk(key),也就是通過MD5生成唯一的請求標示,這樣就可以通過key來獲取DiskLruCache.Editor實例。獲取到實例後就可以獲取到OutputStream,然後通過BufferedWriter寫入,如下代碼

    public void put(String key, String value)
    {
        DiskLruCache.Editor edit = null;
        BufferedWriter bw = null;
        try
        {
            edit = editor(key);
            if (edit == null) return;
            OutputStream os = edit.newOutputStream(0);
            bw = new BufferedWriter(new OutputStreamWriter(os));
            bw.write(value);
            edit.commit();//write CLEAN
        } catch (IOException e)
        {
            e.printStackTrace();
            try
            {
                //s
                edit.abort();//write REMOVE
            } catch (IOException e1)
            {
                e1.printStackTrace();
            }
        } finally
        {
            try
            {
                if (bw != null)
                    bw.close();
            } catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }

讀取緩存

首先是通過key獲取DiskLruCache.Snapshot實例,然後得到InputStream,如下代碼

    public InputStream get(String key)
    {
        try
        {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(Utils.hashKeyForDisk(key));
            if (snapshot == null) //not find entry , or entry.readable = false
            {
                Log.e(TAG, "not find entry , or entry.readable = false");
                return null;
            }
            //write READ
            return snapshot.getInputStream(0);

        } catch (IOException e)
        {
            e.printStackTrace();
            return null;
        }

    }

然後就是InputStreamReader讀取,如下代碼

    public String getAsString(String key) {
        InputStream inputStream = null;
        inputStream = get(key);
        if (inputStream == null) return null;
        String str = null;
        try {
            str = Util.readFully(new InputStreamReader(inputStream, Util.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
            try {
                inputStream.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
        return str;
    }

    static String readFully(Reader reader) throws IOException
    {
        try
        {
            StringWriter writer = new StringWriter();
            char[] buffer = new char[1024];
            int count;
            while ((count = reader.read(buffer)) != -1)
            {
                writer.write(buffer, 0, count);
            }
            return writer.toString();
        } finally
        {
            reader.close();
        }
    }

然後就是刪除操作

    public boolean remove(String key)
    {
        try
        {
            key = Utils.hashKeyForDisk(key);
            return mDiskLruCache.remove(key);
        } catch (IOException e)
        {
            e.printStackTrace();
        }
        return false;
    }

直接remove掉就ok了。

DiskLruCache的封裝

從Github裡面搜索DiskLruCache,可以看到鴻洋大神的base-diskcache框架,它主要是把diskcache封裝成和AsimpleCache框架一樣,挺好用的。
使用方法如下(來源於base-diskcache框架)

存

put(String key, Bitmap bitmap)

put(String key, byte[] value)

put(String key, String value)

put(String key, JSONObject jsonObject)

put(String key, JSONArray jsonArray)

put(String key, Serializable value)

put(String key, Drawable value)

editor(String key).newOutputStream(0);//原有的方式


取

String getAsString(String key);

JSONObject getAsJson(String key)

JSONArray getAsJSONArray(String key)

 T getAsSerializable(String key)

Bitmap getAsBitmap(String key)

byte[] getAsBytes(String key)

Drawable getAsDrawable(String key)

InputStream get(String key);//原有的用法

這裡我只是保存響應的json,只用到

put(String key, String value)

String getAsString(String key);

兩個方法,至於key使用請求參數生成的MD5做為唯一的標示。

下面就使用這個DiskLruCache封裝進行手動緩存,DiskLruCache的源碼和封裝代碼可以去鴻洋的github上下載。

HRetrofitNetHelper代碼的修改

基於上一篇博客的HRetrofitNetHelper對象。進行代碼修改,修改點如下

去除OkHttp的cache緩存配置 去除mUrlInterceptor的攔截器 改在call的onresponse裡面進行操作 enqueueCall方法配置成鏈式編程配置

然後再貼上全部的代碼,注意幾個修改點就好了。

public class HRetrofitNetHelper implements HttpLoggingInterceptor.Logger {
    public static HRetrofitNetHelper mInstance;
    public Retrofit mRetrofit;
    public OkHttpClient mOkHttpClient;
    public HttpLoggingInterceptor mHttpLogInterceptor;
    private BasicParamsInterceptor mBaseParamsInterceptor;
    private  Context mContext;
    public  Gson mGson;
    //DiskLruCache封裝的幫助類,
    private  DiskLruCacheHelper diskLruCacheHelper;
    public static final String BASE_URL = "http://192.168.1.102:8080/GoachWeb/";
    private Action1 onNextAction;
    private HRetrofitNetHelper(Context context){
        this.mContext = context ;
        createSubscriberByAction();
        mGson = new GsonBuilder()
                .setDateFormat("yyyy-MM-dd HH:mm:ss")
                .create();
        mHttpLogInterceptor = new HttpLoggingInterceptor(this);
        mHttpLogInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        Map tempParams = getBaseParams();
        mBaseParamsInterceptor = new BasicParamsInterceptor.Builder()
                .addParamsMap(tempParams)
                .build();
        try {
        //創建DiskLruCacheHelper 對象
            diskLruCacheHelper = new DiskLruCacheHelper(mContext);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //這裡去除了緩存配置和mUrlInterceptor的配置
        mOkHttpClient = new OkHttpClient.Builder()
                .connectTimeout(12, TimeUnit.SECONDS)
                .writeTimeout(20, TimeUnit.SECONDS)
                .readTimeout(20, TimeUnit.SECONDS)
                .retryOnConnectionFailure(true)
                .addInterceptor(mHttpLogInterceptor)
                .addInterceptor(mBaseParamsInterceptor)
                .build();
        mRetrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(mGson))
                .client(mOkHttpClient)
                .build();
    }
    public static HRetrofitNetHelper getInstance(Context context){
        if(mInstance==null){
            synchronized (HRetrofitNetHelper.class){
                if(mInstance==null){
                    mInstance =  new HRetrofitNetHelper(context);
                }
            }
        }
        return mInstance;
    }
    public  T getAPIService(Class service) {
        return mRetrofit.create(service);
    }
    /*這裡改成鏈式編程,默認是不緩存。在不緩存的情況下,只需配置Call>實例,也就是調用上面getAPIService方法獲取的實例。然後就是retrofitCallBack回調接口,如果需要緩存的情況,那麼就要再配置isCache為true,然後配置Type(主要是Gson解析泛型會報錯Java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to,所以需再傳遞這個參數進行解析),最後調用start方法進行請求*/
    public static final class enqueueCall{
        boolean isCache;
        Type clazz;
        Call call;
        RetrofitCallBack retrofitCallBack;
        HRetrofitNetHelper mRetrofitNetHelper;
        private Context mContext;
        public  Gson mGson;
        private DiskLruCacheHelper diskLruCacheHelper;
        public enqueueCall(HRetrofitNetHelper retrofitNetHelper){
            isCache = false;
            this.mRetrofitNetHelper = retrofitNetHelper;
            this.mContext = retrofitNetHelper.mContext;
            this.mGson = retrofitNetHelper.mGson;
            this.diskLruCacheHelper = retrofitNetHelper.diskLruCacheHelper;
        }
        public  enqueueCall call(Call> call){
            this.call = call ;
            return this;
        }
        public enqueueCall clazz(Type clazz){
            this.clazz = clazz ;
            return this;
        }
        public  enqueueCall retrofitCallBack(RetrofitCallBack retrofitCallBack){
            this.retrofitCallBack = retrofitCallBack ;
            return this;
        }
        public enqueueCall isCache(boolean isCache){
            this.isCache = isCache ;
            return this;
        }
        public  enqueueCall start(){
            call.enqueue(new Callback>() {
                @Override
                public void onResponse(Call> call, Response> response) {
                    //獲取請求Request 
                    Request request = call.request();
                    //獲取請求的url
                    String requestUrl = call.request().url().toString();
                    //去獲取返回數據
                    BaseResp resp = response.body() ;
                    //去獲取RequestBody
                    RequestBody requestBody = request.body();
                    //緩存格式為utf-8
                    Charset charset = Charset.forName("UTF-8");
                    //去獲取要保存的key
                     String key="";
                     //如果是Post請求,要通過Buffer去讀取body體裡面的參數
                    if(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();
                        }
                        key = buffer.readString(charset);

                        buffer.close();
                    }else{
                    //如果不是Post請求,比如Get請求,那麼久通過url做為唯一標識
                        key = requestUrl;
                    }
                    Log.d("zgx","response==========key"+key);
                    //處理特殊接口,如果是登錄接口進行彈框提示
                    if(!TextUtils.isEmpty(requestUrl)){
                        if(requestUrl.contains("LoginDataServlet")) {
                            if (Looper.myLooper() == null) {
                                Looper.prepare();
                            }
                            mRetrofitNetHelper.createObservable("現在請求的是登錄接口");
                        }
                    }
                    //分為有網和沒網的情況下
                    //如果有網
                    if(NetUtil.checkNetwork(mContext)!=NetUtil.NO_NETWORK){
                        //如果返回數據為null
                        if(resp==null){
                        //回調失敗接口
                            if(retrofitCallBack!=null)
                                retrofitCallBack.onFailure("暫無數據");
                        }else{
                        //如果是接口返回2000或者2001或者2002,進行彈框提示
                            if (resp.getResultCode() == 2000 || resp.getResultCode() == 2001 || resp.getResultCode() == 2002) {
                                Toast.makeText(mContext,"code====="+resp.getResultCode(),Toast.LENGTH_SHORT).show();
                            }
                            //如果接口返回200,並且http請求code返回200,說明請求成功
                            if (resp.getResultCode() == 200&&response.code()==200) {
                                if(retrofitCallBack!=null){
                                    //需要緩存數據
                                    String cacheResponse = mGson.toJson(resp);
 //判斷下當前是否存在key緩存的數據,如果存在移除掉,                                   if(!TextUtils.isEmpty(key)&&!TextUtils.isEmpty(diskLruCacheHelper.getAsString(key)))
                                        diskLruCacheHelper.remove(key);
 //當需要緩存的數據不為空的時候,並且需要緩存的時候,通過diskLruCacheHelper進行緩存                                   if(!TextUtils.isEmpty(key)&&!TextUtils.isEmpty(cacheResponse)&&isCache){
                                        Log.d("zgx","response========cacheResponse"+cacheResponse);
                                        diskLruCacheHelper.put(key,cacheResponse);
                                    }
                                    //然後就是回調成功接口
                                    retrofitCallBack.onSuccess(resp);
                                }
                            } else {
                            //這個是請求失敗,那麼就回調失敗接口
                                // ToastMaker.makeToast(mContext, resp.errMsg, Toast.LENGTH_SHORT);
                                if(retrofitCallBack!=null)
                                    retrofitCallBack.onFailure(resp.getErrMsg());
                            }
                        }
                        return;
                    }
                    //沒有網絡的情況下,去獲取key對應的緩存
                    String json = diskLruCacheHelper.getAsString(key);
                    //如果緩存不存在,那麼久回調失敗接口
                    if(json==null){
                        Toast.makeText(mContext, "沒有緩存!", Toast.LENGTH_SHORT).show();
                        if(retrofitCallBack!=null){
                            retrofitCallBack.onFailure("沒有緩存!");
                        }
                    }else{
                    //判斷是否配置clazz,一定要先配置,要不然Gson解析出錯
                        if(clazz==null){
                            throw new IllegalArgumentException("請先配置clazz");
                        }
                        //解析緩存數據,然後進行回調成功接口
                        resp = mGson.fromJson(json,clazz);
                        if(retrofitCallBack!=null){
                            retrofitCallBack.onSuccess(resp);
                        }
                    }
                }

                @Override
                public void onFailure(Call> call, Throwable t) {
                    //   ToastMaker.makeToast(mContext, "網絡錯誤,請重試!", Toast.LENGTH_SHORT);
                    if(retrofitCallBack!=null){
                        retrofitCallBack.onFailure(t.toString());
                    }
                }
            });
            return this;
        }
    }
    //.....省略,和上篇博客代碼一樣
    //這裡我們改成通過diskLruCacheHelper封裝的類進行刪除緩存
    public void clearCache() throws IOException {
        diskLruCacheHelper.delete();
    }
}

主要修改的地方,上面基本上都注釋到了,這裡沒有做緩存的過期時間,有網的情況下,還是保持數據的實時性,沒網的情況下才會去讀取緩存。

API修改為Post請求

ILoginService.class

public interface ILoginService {
    @FormUrlEncoded
    @POST("LoginDataServlet")
    Call> userLogin(@Field("username") String username, @Field("password") String password);
}

INewsService.class

public interface INewsService {
    @FormUrlEncoded
    @POST("NewsDataServlet")
    Call>> userNews(@Field("userId") String userId);
}

這裡主要是測試這兩個接口

請求修改為鏈式編程

登錄請求修改代碼如下

首先實現回調接口

//傳入成功回調的BaseResp的泛型T為RegisterBean
implements HRetrofitNetHelper.RetrofitCallBack

然後是Call請求配置

final Call> repos = loginService.userLogin(username,password);
      new HRetrofitNetHelper
       .enqueueCall(HRetrofitNetHelper.getInstance(this))
       .call(repos)//repos指的是retrofitNetHelper.getAPIService返回的API
       .retrofitCallBack(this)//配置回調接口
       .isCache(true)//設置需要緩存
       .clazz(new TypeToken>(){}.getType())//Gson解析緩存需要
       .start();//真正開始發起請求

然後實現兩個回調方法

     @Override
    public void onSuccess(BaseResp baseResp) {
        Date date = baseResp.getResponseTime();
        if(baseResp.getData().getErrorCode()==1){
            Toast.makeText(getBaseContext(),"登錄成功",Toast.LENGTH_SHORT).show();
        }else {
            Toast.makeText(getBaseContext(),"用戶不存在",Toast.LENGTH_SHORT).show();
        }
        mDialog.dismiss();
    }

    @Override
    public void onFailure(String error) {
        Log.d("zgx","onFailure======"+error);
        mDialog.dismiss();
    }

如果新聞頁也要緩存,那麼代碼同理修改如下。

    private void loadData(){
        INewsService newService = retrofitNetHelper.getAPIService(INewsService.class);
        Log.d("zgx","mUserId====="+mUserId);
        final Call>> repos = newService.userNews(mUserId);
        new HRetrofitNetHelper.enqueueCall(HRetrofitNetHelper.getInstance(this))
                .call(repos)
                .retrofitCallBack(this)
                .isCache(true)
                .clazz(new TypeToken>>(){}.getType())
                .start();
    }

這樣就緩存了登錄接口的數據和新聞頁面的數據。
下面就來測試下,只緩存登錄接口。測試結果為有網的情況下,根據上面代碼知道登錄成功會彈出登錄成功的Toast,並且會生成緩存文件,沒有網絡的情況下會去讀取緩存,並且還是會彈出Toast提示,登錄失敗不彈。效果如下

這裡寫圖片描述

接下來我們再看下沒有緩存的效果,代碼只要修改不配置<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> HRetrofitNetHelper.enqueueCall(HRetrofitNetHelper.getInstance(this)) .call(repos) .retrofitCallBack(this) .start();

然後就來看效果,有網的情況下應該為登錄成功,沒網的情況下,提示沒有緩存,效果如下

這裡寫圖片描述

Get請求效果同理。同樣可以得到這樣的效果,感興趣的可以去試下。

最後配置3個權限

 
 
 

總體感覺Retrofit+OkHttp框架用起來還是很方便的。特別是響應式編程,用的特別爽。還有就是Retrofit的源碼設計的特別完美。不過在這裡,用RxAndroid用的還是比較少,相信以後會用的越來越多,而且現在谷歌的agera響應式編程也出來了。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved