优雅的构建 Android 项目之磁盘缓存(DiskLruCache)

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主页

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容