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() 方法中调用,它的作用是同步缓存的日志文件,没必要每次缓存都调用