優雅的構建 Android 專案之磁碟快取(DiskLruCache)

PandaQ發表於2019-03-04

Android 的快取技術

一個優秀的應用首先它的使用者體驗是優秀的,在 Android 應用中恰當的使用快取技術不僅可以緩解伺服器壓力還可以優化使用者的使用體驗,減少使用者流量的使用。在 Android 中快取分為記憶體快取和磁碟快取兩種:

記憶體快取

  • 讀取速度快
  • 可分配空間小
  • 有被系統回收風險
  • 應用退出就沒有了,無法做到離線快取

    磁碟快取

  • 讀取速度比記憶體快取慢
  • 可分配空間較大
  • 不會因為系統記憶體緊張而被系統回收
  • 退出應用快取仍然存在(快取在應用對應的磁碟目錄中解除安裝時會一同清理,快取在其他位置解除安裝會有殘留)
    本文主要介紹磁碟快取,並以快取 MVPDemo 中的知乎日報新聞條目作為事例展示如何使用磁碟快取對新聞列表進行快取。

DiskLruCache

DiskLruCache 是 JakeWharton 大神在 github 上的一個開源庫,程式碼量並不多。與谷歌官方的記憶體快取策略LruCache 相對應,DiskLruCache 也遵從於 LRU(Least recently used 最近最少使用)演算法,只不過儲存位置在磁碟上。雖然在谷歌的文件中有提到但 DiskLruCache 並未整合到官方的 API中,使用的話按照 github 庫中的方式整合就行。
DiskLruCache 使用時需要注意:

  • 每一條快取都有一個 String 型別的 key 與之對應,每一個 key 中的值都必須滿足 [a-z0-9_-]{1,120}的規則即數字大小寫字母長度在1-120之間,所以推薦將字串譬如圖片的 url 等進行 MD5 加密後作為 key。
  • DiskLruCache 的資料是快取在檔案系統的某一目錄中的,這個目錄必須是唯一對應某一條快取的,快取可能會重寫和刪除目錄中的檔案。多個程式同一時間使用同一個快取目錄會出錯。
  • DiskLruCache 遵從 LRU 演算法,當快取資料達到設定的極限值時將會後臺自動按照 LRU 演算法移除快取直到滿足存下新的快取不超過極限值。
  • 一條快取記錄一次只能有一個 editor ,如果值不可編輯將會返回一個空值。
  • 當一條快取建立時,應該提供完整的值,如果是空值的話使用佔位符代替。
  • 如果檔案從檔案系統中丟失,相應的條目將從快取中刪除。如果寫入快取值時出錯,編輯將失敗。

使用方法

開啟快取

DiskLruCache 不能使用 new 的方式建立,建立一個快取物件方式如下:

/**
*引數說明
*
*cacheFile 快取檔案的儲存路徑
*appVersion 應用版本號。DiskLruCache 認為應用版本更新後所有的資料都因該從伺服器重新拉取,因此需要版本號進行判斷
*1 每條快取條目對應的值的個數,這裡設定為1個。
*Constants.CACHE_MAXSIZE 我自己定義的常量類中的值表示換粗的最大儲存空間
**/
DiskLruCache mDiskLruCache = DiskLruCache.open(cacheFile, appVersion, 1, Constants.CACHE_MAXSIZE);複製程式碼

存入快取

DiskLruCache 存快取是通過 DiskLruCache.Editor 處理的:

/**
 *此處是為程式碼,實際使用還需要 try catch 處理可能出現的異常
 *
 **/
String key = getMD5Result(key);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
OutputStream os = editor.newOutputStream(0);
//此處存的一個 新聞物件因此用 ObjectOutputStream
ObjectOutputStream outputStream = new ObjectOutputStream(os);
outputStream.writeObject(stories);
//別忘了關閉流和提交編輯
outputStream.close();
editor.commit();複製程式碼

取出快取

DiskLruCache 取快取是通過 DiskLruCache.Snapshot 處理的:

/**
 *此處是為程式碼,實際使用還需要 try catch 處理可能出現的異常
 *
 **/
String key = getMD5Result(key);
//通過設定的 key 去獲取縮略物件
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
//通過 SnapShot 物件獲取流資料
InputStream in = snapshot.getInputStream(0);
ObjectInputStream ois = new ObjectInputStream(in);
//將流資料轉換為 Object 物件
ArrayList<ZhihuStory> stories = (ArrayList<ZhihuStory>) ois.readObject();複製程式碼

使用 DiskLruCache 進行磁碟快取基本流程就這樣,開——>存 或者 開——>取。

完整流程的程式碼

    //使用rxandroid+retrofit進行請求
    public void loadDataByRxandroidRetrofit() {
        mINewsListActivity.showProgressBar();
        Subscription subscription = ApiManager.getInstence().getDataService()
                .getZhihuDaily()
                .map(new Func1<ZhiHuDaily, ArrayList<ZhihuStory>>() {
                    @Override
                    public ArrayList<ZhihuStory> call(ZhiHuDaily zhiHuDaily) {
                        ArrayList<ZhihuStory> stories = zhiHuDaily.getStories();
                        if (stories != null) {
                            //載入成功後將資料快取倒本地(demo 中只有一頁,實際使用時根據需求選擇是否進行快取)
                            makeCache(zhiHuDaily.getStories());
                        }
                        return stories;
                    }
                })
                //設定事件觸發在非主執行緒
                .subscribeOn(Schedulers.io())
                //設定事件接受在UI執行緒以達到UI顯示的目的
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<ArrayList<ZhihuStory>>() {
                    @Override
                    public void onCompleted() {
                        mINewsListActivity.hidProgressBar();
                    }

                    @Override
                    public void onError(Throwable e) {
                        mINewsListActivity.getDataFail("", e.getMessage());
                    }

                    @Override
                    public void onNext(ArrayList<ZhihuStory> stories) {
                        mINewsListActivity.getDataSuccess(stories);
                    }
                });
        //繫結觀察物件,注意在介面的ondestory或者onpouse方法中呼叫presenter.unsubcription();
        addSubscription(subscription);
    }

    //生成Cache
    private void makeCache(ArrayList<ZhihuStory> stories) {
        File cacheFile = getCacheFile(MyApplication.getContext(), Constants.ZHIHUCACHE);
        DiskLruCache diskLruCache = DiskLruCache.open(cacheFile, MyApplication.getAppVersion(), 1, Constants.CACHE_MAXSIZE);
        try {
            //使用MD5加密後的字串作為key,避免key中有非法字元
            String key = SecretUtil.getMD5Result(Constants.ZHIHUSTORY_KEY);
            DiskLruCache.Editor editor = diskLruCache.edit(key);
            if (editor != null) {
                ObjectOutputStream outputStream = new ObjectOutputStream(editor.newOutputStream(0));
                outputStream.writeObject(stories);
                outputStream.close();
                editor.commit();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //載入Cache
    public void loadCache() {
        File cacheFile = getCacheFile(MyApplication.getContext(), Constants.ZHIHUCACHE);
        DiskLruCache diskLruCache = DiskLruCache.open(cacheFile, MyApplication.getAppVersion(), 1, Constants.CACHE_MAXSIZE);
        String key = SecretUtil.getMD5Result(Constants.ZHIHUSTORY_KEY);
        try {
            DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
            if (snapshot != null) {
                InputStream in = snapshot.getInputStream(0);
                ObjectInputStream ois = new ObjectInputStream(in);
                try {
                    ArrayList<ZhihuStory> stories = (ArrayList<ZhihuStory>) ois.readObject();
                    if (stories != null) {
                        mINewsListActivity.getDataSuccess(stories);
                    } else {
                        mINewsListActivity.getDataFail("", "無資料");
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //獲取Cache 儲存目錄
    private  File getCacheFile(Context context, String uniqueName) {
        String cachePath = null;
        if ((Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable())
                && context.getExternalCacheDir() != null) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }複製程式碼

上面的程式碼跑通流程存 Cache 取 Cache 是沒有問題的,但是這麼寫肯定是不優雅的!兩年前的我可能會將這樣的程式碼作為釋出程式碼。

方法封裝,優雅的使用

既有 key 又有 value 還有 Editor 的你想到了什麼?應該是 SharePreferences 吧!在 MVPDemo 中我構建了一個 DiskLruCacheManager 類來封裝 Cache 的存取。程式碼就不貼了,大家自行在 demo 中檢視 DiskManager 類,我只說一下怎麼使用它來存取 Cache:

存取都一樣需要先拿到 DiskManager 的例項

DiskCacheManager manager = new DiskCacheManager(MyApplication.getContext(), Constants.ZHIHUCACHE);複製程式碼

然後通過 manager 的公共方法進行資料的存取:

資料型別 存入方法 取出方法 說明
String put(String key,String value) getString(String key) 返回String物件
JsonObject put(String key,JsonObject value) getJsonObject(String key) 內部實際是轉換成String存取
JsonArray put(String key,JsonArray value) getJsonArray(String key) 內部實際是轉換成String存取
byte[] put(String key,byte[] bytes) getBytes(String key) 存圖片用這個實現,大家自行封裝啦
Serializable put(String key,Serializable value) getSerializable(String key) 返回的是一個泛型物件

manager.flush() 方法推薦在需要快取的介面的 onpause() 方法中呼叫,它的作用是同步快取的日誌檔案,沒必要每次快取都呼叫

最後

覺得本文對你有幫助
關注簡書PandaQ404,持續分享中。。。
Github主頁

相關文章