優(yōu)雅的構(gòu)建 Android 項(xiàng)目之磁盤(pán)緩存(DiskLruCache)

Android 的緩存技術(shù)

一個(gè)優(yōu)秀的應(yīng)用首先它的用戶體驗(yàn)是優(yōu)秀的冀泻,在 Android 應(yīng)用中恰當(dāng)?shù)氖褂镁彺婕夹g(shù)不僅可以緩解服務(wù)器壓力還可以優(yōu)化用戶的使用體驗(yàn)迁客,減少用戶流量的使用席怪。在 Android 中緩存分為內(nèi)存緩存和磁盤(pán)緩存兩種:

內(nèi)存緩存

  • 讀取速度快
  • 可分配空間小
  • 有被系統(tǒng)回收風(fēng)險(xiǎn)
  • 應(yīng)用退出就沒(méi)有了,無(wú)法做到離線緩存

磁盤(pán)緩存

  • 讀取速度比內(nèi)存緩存慢
  • 可分配空間較大
  • 不會(huì)因?yàn)橄到y(tǒng)內(nèi)存緊張而被系統(tǒng)回收
  • 退出應(yīng)用緩存仍然存在(緩存在應(yīng)用對(duì)應(yīng)的磁盤(pán)目錄中卸載時(shí)會(huì)一同清理,緩存在其他位置卸載會(huì)有殘留)
    本文主要介紹磁盤(pán)緩存具篇,并以緩存 MVPDemo 中的知乎日?qǐng)?bào)新聞條目作為事例展示如何使用磁盤(pán)緩存對(duì)新聞列表進(jìn)行緩存拳氢。

DiskLruCache

DiskLruCache 是 JakeWharton 大神在 github 上的一個(gè)開(kāi)源庫(kù)募逞,代碼量并不多。與谷歌官方的內(nèi)存緩存策略LruCache 相對(duì)應(yīng)馋评,DiskLruCache 也遵從于 LRU(Least recently used 最近最少使用)算法放接,只不過(guò)存儲(chǔ)位置在磁盤(pán)上。雖然在谷歌的文檔中有提到但 DiskLruCache 并未集成到官方的 API中留特,使用的話按照 github 庫(kù)中的方式集成就行纠脾。
DiskLruCache 使用時(shí)需要注意:

  • 每一條緩存都有一個(gè) String 類型的 key 與之對(duì)應(yīng),每一個(gè) key 中的值都必須滿足 [a-z0-9_-]{1,120}的規(guī)則即數(shù)字大小寫(xiě)字母長(zhǎng)度在1-120之間蜕青,所以推薦將字符串譬如圖片的 url 等進(jìn)行 MD5 加密后作為 key苟蹈。
  • DiskLruCache 的數(shù)據(jù)是緩存在文件系統(tǒng)的某一目錄中的,這個(gè)目錄必須是唯一對(duì)應(yīng)某一條緩存的右核,緩存可能會(huì)重寫(xiě)和刪除目錄中的文件慧脱。多個(gè)進(jìn)程同一時(shí)間使用同一個(gè)緩存目錄會(huì)出錯(cuò)。
  • DiskLruCache 遵從 LRU 算法贺喝,當(dāng)緩存數(shù)據(jù)達(dá)到設(shè)定的極限值時(shí)將會(huì)后臺(tái)自動(dòng)按照 LRU 算法移除緩存直到滿足存下新的緩存不超過(guò)極限值菱鸥。
  • 一條緩存記錄一次只能有一個(gè) editor 宗兼,如果值不可編輯將會(huì)返回一個(gè)空值。
  • 當(dāng)一條緩存創(chuàng)建時(shí)采缚,應(yīng)該提供完整的值针炉,如果是空值的話使用占位符代替。
  • 如果文件從文件系統(tǒng)中丟失扳抽,相應(yīng)的條目將從緩存中刪除篡帕。如果寫(xiě)入緩存值時(shí)出錯(cuò),編輯將失敗贸呢。

使用方法

打開(kāi)緩存

DiskLruCache 不能使用 new 的方式創(chuàng)建镰烧,創(chuàng)建一個(gè)緩存對(duì)象方式如下:

/**
*參數(shù)說(shuō)明
*
*cacheFile 緩存文件的存儲(chǔ)路徑
*appVersion 應(yīng)用版本號(hào)。DiskLruCache 認(rèn)為應(yīng)用版本更新后所有的數(shù)據(jù)都因該從服務(wù)器重新拉取楞陷,因此需要版本號(hào)進(jìn)行判斷
*1 每條緩存條目對(duì)應(yīng)的值的個(gè)數(shù)怔鳖,這里設(shè)置為1個(gè)。
*Constants.CACHE_MAXSIZE 我自己定義的常量類中的值表示換粗的最大存儲(chǔ)空間
**/
DiskLruCache mDiskLruCache = DiskLruCache.open(cacheFile, appVersion, 1, Constants.CACHE_MAXSIZE);

存入緩存

DiskLruCache 存緩存是通過(guò) DiskLruCache.Editor 處理的:

/**
 *此處是為代碼固蛾,實(shí)際使用還需要 try catch 處理可能出現(xiàn)的異常
 *
 **/
String key = getMD5Result(key);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
OutputStream os = editor.newOutputStream(0);
//此處存的一個(gè) 新聞對(duì)象因此用 ObjectOutputStream
ObjectOutputStream outputStream = new ObjectOutputStream(os);
outputStream.writeObject(stories);
//別忘了關(guān)閉流和提交編輯
outputStream.close();
editor.commit();

取出緩存

DiskLruCache 取緩存是通過(guò) DiskLruCache.Snapshot 處理的:

/**
 *此處是為代碼结执,實(shí)際使用還需要 try catch 處理可能出現(xiàn)的異常
 *
 **/
String key = getMD5Result(key);
//通過(guò)設(shè)置的 key 去獲取縮略對(duì)象
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
//通過(guò) SnapShot 對(duì)象獲取流數(shù)據(jù)
InputStream in = snapshot.getInputStream(0);
ObjectInputStream ois = new ObjectInputStream(in);
//將流數(shù)據(jù)轉(zhuǎn)換為 Object 對(duì)象
ArrayList<ZhihuStory> stories = (ArrayList<ZhihuStory>) ois.readObject();

使用 DiskLruCache 進(jìn)行磁盤(pán)緩存基本流程就這樣,開(kāi)——>存 或者 開(kāi)——>取艾凯。

完整流程的代碼

    //使用rxandroid+retrofit進(jìn)行請(qǐng)求
    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) {
                            //加載成功后將數(shù)據(jù)緩存倒本地(demo 中只有一頁(yè)献幔,實(shí)際使用時(shí)根據(jù)需求選擇是否進(jìn)行緩存)
                            makeCache(zhiHuDaily.getStories());
                        }
                        return stories;
                    }
                })
                //設(shè)置事件觸發(fā)在非主線程
                .subscribeOn(Schedulers.io())
                //設(shè)置事件接受在UI線程以達(dá)到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);
                    }
                });
        //綁定觀察對(duì)象,注意在界面的ondestory或者onpouse方法中調(diào)用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("", "無(wú)數(shù)據(jù)");
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    //獲取Cache 存儲(chǔ)目錄
    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 是沒(méi)有問(wèn)題的蜡感,但是這么寫(xiě)肯定是不優(yōu)雅的!兩年前的我可能會(huì)將這樣的代碼作為發(fā)布代碼恃泪。

方法封裝郑兴,優(yōu)雅的使用

既有 key 又有 value 還有 Editor 的你想到了什么?應(yīng)該是 SharePreferences 吧贝乎!在 MVPDemo 中我構(gòu)建了一個(gè) DiskLruCacheManager 類來(lái)封裝 Cache 的存取情连。代碼就不貼了,大家自行在 demo 中查看 DiskManager 類览效,我只說(shuō)一下怎么使用它來(lái)存取 Cache:

存取都一樣需要先拿到 DiskManager 的實(shí)例

DiskCacheManager manager = new DiskCacheManager(MyApplication.getContext(), Constants.ZHIHUCACHE);

然后通過(guò) manager 的公共方法進(jìn)行數(shù)據(jù)的存让删摺:

數(shù)據(jù)類型 存入方法 取出方法 說(shuō)明
String put(String key,String value) getString(String key) 返回String對(duì)象
JsonObject put(String key,JsonObject value) getJsonObject(String key) 內(nèi)部實(shí)際是轉(zhuǎn)換成String存取
JsonArray put(String key,JsonArray value) getJsonArray(String key) 內(nèi)部實(shí)際是轉(zhuǎn)換成String存取
byte[] put(String key,byte[] bytes) getBytes(String key) 存圖片用這個(gè)實(shí)現(xiàn),大家自行封裝啦
Serializable put(String key,Serializable value) getSerializable(String key) 返回的是一個(gè)泛型對(duì)象

manager.flush() 方法推薦在需要緩存的界面的 onpause() 方法中調(diào)用朽肥,它的作用是同步緩存的日志文件,沒(méi)必要每次緩存都調(diào)用

最后

覺(jué)得本文對(duì)你有幫助
關(guān)注簡(jiǎn)書(shū)PandaQ404持钉,持續(xù)分享中衡招。。每强。
Github主頁(yè)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末始腾,一起剝皮案震驚了整個(gè)濱河市州刽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌浪箭,老刑警劉巖穗椅,帶你破解...
    沈念sama閱讀 223,002評(píng)論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異奶栖,居然都是意外死亡匹表,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門(mén)宣鄙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)袍镀,“玉大人,你說(shuō)我怎么就攤上這事冻晤∥郏” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,787評(píng)論 0 365
  • 文/不壞的土叔 我叫張陵鼻弧,是天一觀的道長(zhǎng)设江。 經(jīng)常有香客問(wèn)我,道長(zhǎng)攘轩,這世上最難降的妖魔是什么叉存? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,237評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮撑刺,結(jié)果婚禮上鹉胖,老公的妹妹穿的比我還像新娘。我一直安慰自己够傍,他們只是感情好甫菠,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,237評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著冕屯,像睡著了一般寂诱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上安聘,一...
    開(kāi)封第一講書(shū)人閱讀 52,821評(píng)論 1 314
  • 那天痰洒,我揣著相機(jī)與錄音,去河邊找鬼浴韭。 笑死丘喻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的念颈。 我是一名探鬼主播泉粉,決...
    沈念sama閱讀 41,236評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了嗡靡?” 一聲冷哼從身側(cè)響起跺撼,我...
    開(kāi)封第一講書(shū)人閱讀 40,196評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎讨彼,沒(méi)想到半個(gè)月后歉井,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,716評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哈误,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,794評(píng)論 3 343
  • 正文 我和宋清朗相戀三年哩至,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黑滴。...
    茶點(diǎn)故事閱讀 40,928評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡憨募,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出袁辈,到底是詐尸還是另有隱情菜谣,我是刑警寧澤,帶...
    沈念sama閱讀 36,583評(píng)論 5 351
  • 正文 年R本政府宣布晚缩,位于F島的核電站尾膊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏荞彼。R本人自食惡果不足惜冈敛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,264評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鸣皂。 院中可真熱鬧抓谴,春花似錦、人聲如沸寞缝。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,755評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)荆陆。三九已至滩届,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間被啼,已是汗流浹背帜消。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,869評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浓体,地道東北人泡挺。 一個(gè)月前我還...
    沈念sama閱讀 49,378評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像命浴,于是被迫代替她去往敵國(guó)和親粘衬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,937評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容