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è)