優雅的構建 Android 項目之磁盤緩存(DiskLruCache)

Android 的緩存技術

一個優秀的應用首先它的用戶體驗是優秀的,在 Android 應用中恰當的使用緩存技術不只能夠緩解服務器壓力還能夠優化用戶的使用體驗,減小用戶流量的使用。在 Android 中緩存分爲內存緩存和磁盤緩存兩種:java

內存緩存

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

    磁盤緩存

  • 讀取速度比內存緩存慢
  • 可分配空間較大
  • 不會由於系統內存緊張而被系統回收
  • 退出應用緩存仍然存在(緩存在應用對應的磁盤目錄中卸載時會一同清理,緩存在其餘位置卸載會有殘留)
    本文主要介紹磁盤緩存,並以緩存 MVPDemo 中的知乎日報新聞條目做爲事例展現如何使用磁盤緩存對新聞列表進行緩存。

DiskLruCache

DiskLruCache 是 JakeWharton 大神在 github 上的一個開源庫,代碼量並很少。與谷歌官方的內存緩存策略LruCache 相對應,DiskLruCache 也聽從於 LRU(Least recently used 最近最少使用)算法,只不過存儲位置在磁盤上。雖然在谷歌的文檔中有提到但 DiskLruCache 並未集成到官方的 API中,使用的話按照 github 庫中的方式集成就行。
DiskLruCache 使用時須要注意:android

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

使用方法

打開緩存

DiskLruCache 不能使用 new 的方式建立,建立一個緩存對象方式以下:git

/** *參數說明 * *cacheFile 緩存文件的存儲路徑 *appVersion 應用版本號。DiskLruCache 認爲應用版本更新後全部的數據都因該從服務器從新拉取,所以須要版本號進行判斷 *1 每條緩存條目對應的值的個數,這裏設置爲1個。 *Constants.CACHE_MAXSIZE 我本身定義的常量類中的值表示換粗的最大存儲空間 **/
DiskLruCache mDiskLruCache = DiskLruCache.open(cacheFile, appVersion, 1, Constants.CACHE_MAXSIZE);複製代碼

存入緩存

DiskLruCache 存緩存是經過 DiskLruCache.Editor 處理的:github

/** *此處是爲代碼,實際使用還須要 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:app

存取都同樣須要先拿到 DiskManager 的實例

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

而後經過 manager 的公共方法進行數據的存取:ide

數據類型 存入方法 取出方法 說明
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主頁

相關文章
相關標籤/搜索