Android的缓存策略:LruCache和DiskLruCache

当用户要浏览微信的某张图片或视频时,第一次肯定需要从网络上下载下来才能看,但如果第二次去浏览,还要从网络上下载就不合适了,用户体验差,更重要的是浪费了用户的流量。当图片首次下载下来的时候,我们需要对图片做一下缓存,这样再次读它的时候直接从缓存中取就可以了。图片缓存对于目前的主流图片加载框架(比如UniversalImageLoader)是最最基础的功能了。

缓存通常分为两种:内存缓存和硬盘缓存。当应用打算从网络上请求一张图片时,先尝试从内存中获取,如果没有再尝试从硬盘中获取,还是没有再从网络上下载。因为内存速度>硬盘速度>下载速度,而且还能节省流量。上述的缓存策略不只适用于图片,还适用于其他文件类型。

缓存算法

内存缓存和硬盘缓存的存储空间都是有限的,而且使用缓存时都需要制定一个最大的使用容量。如果超过这个容量,但程序还需要添加缓存,就需要删除一些旧的缓存。目前最常用的一种缓存算法是LRU(Least Recently Used),最近最少使用算法,当缓存满时会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,其中LruCache用于实现内存缓存,DiskLruCache用于实现硬盘缓存。

LruCache

从Android 3.1开始提供这个类,之前的android版本想使用的话可以用support-v4下面的。

LruCache是一个泛型类,内部通过LinkedHashMap以强引用的方式存储缓存对象,它本身也提供了get和put方法供外界调用。另外它是线程安全的:

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;

    public LruCache(int maxSize) {

new一个LruCache对象时,构造函数的参数需指定缓存的总容量大小。下面通过一个小demo来看一下它的使用:

public class MainActivity extends ActionBarActivity {

    private LruCache<String, Bitmap> mLruCache;
    private ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mImageView = (ImageView) findViewById(R.id.iv);

        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 4;
        mLruCache = new LruCache<String, Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };

        Bitmap bitmap;
        String path = new File(Environment.getExternalStorageDirectory(), "a.jpg").getAbsolutePath();
        if((bitmap = BitmapFactory.decodeFile(path)) != null){
            //先缓存后再取出
            mLruCache.put(path, bitmap);
            mImageView.setImageBitmap(mLruCache.get(path));
        }else{
            Toast.makeText(MainActivity.this, "error path", Toast.LENGTH_SHORT).show();
        }
    }
}

这个demo中,设定了缓存容量的总大小为当前进程可用内存的1/4,单位是KB。另外重写了sizeOf方法,它的作用是计算缓存对象的大小,注意它的单位应该跟总容量的单位保持一致,这里都是KB。

一些特殊情况下,还需要重写entryRemoved方法,LruCache移除旧缓存时会调用该方法,因此可以在其中完成一些资源回收工作。

DiskLruCache

这个类并不在android源码中,使用时需要手动把这个文件加入到项目中,它的源码可以从google source中获取:
android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java
如果网址打不开可以点击这里下载源码:点我下载
复制到工程中后注意改一下包名。

这个类的构造方法是私有的,只能通过下面方法去构造对象:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

首先来讲解一下参数的含义:
directory表示要缓存的路径,我们选择路径的时候最好存在/sdcard/Android/data//cache里,因为系统可以识别出这是应用的缓存路径,当程序被卸载时这里的数据会被一起清掉;另外cache下面可以再加一级路径,比如/bitmap/,用来区分不同的缓存对象类型。获取路径可以参考下面的代码:

private File getDiskCacheDir(String folderName) {
    String cachePath;
    if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) && !Environment.isExternalStorageRemovable()){
        cachePath = getExternalCacheDir().getPath();
    }else{
        cachePath = getCacheDir().getPath();
    }
    return new File(cachePath, folderName);
}

appVersion表示当前应用的版本,如果版本号改变了,那么缓存会被清空,数据需要从网上重新获取。获取版本号可以参考下面的代码:

private int getAppVersion() {
    try {
        PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
        return packageInfo.versionCode;
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    return 1;
}

valueCount表示一个key可以对应几个缓存文件,通常设为1。
maxSize表示缓存容量的最大值。

得到DiskLruCache对象后,就可以缓存文件了,比如我们要缓存网上的一个bitmap,具体步骤是:

1.通过DiskLruCache对象获取DiskLruCache.Editor对象,要执行缓存操作必须要用到这个editor对象:

DiskLruCache.Editor editor = mDiskLruCache.edit(cacheKey);

2.通过editor获取输出流,用来存储缓存:

OutputStream os = editor.newOutputStream(0);

这里参数传0是因为创建DiskLruCache时valueCount我们传了1。

3.从网上下载bitmap,将访问url得到的输入流写入到第2步得到的输出流里:

private boolean downloadBitmap(String urlString, OutputStream os) {
    try {
        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        BufferedInputStream bis = new BufferedInputStream(conn.getInputStream(), 4 * 1024);
        BufferedOutputStream bos = new BufferedOutputStream(os, 4 * 1024);
        int len;
        while((len = bis.read()) != -1){
            bos.write(len);
        }
        bis.close();
        bos.close();
        conn.disconnect();
        return true;
    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return false;
}

4.将输出流存入缓存:

if(downloadBitmap(downloadUrl, os)){
    editor.commit();
}else{
    editor.abort();
}
mDiskLruCache.flush();

commit代表提交,即写入生效;abort代表放弃此次操作。调用flush()表示将操作记录都同步到journal文件里,这个journal文件是DiskLruCache的操作记录日志,它的位置也在上面我们指定的缓存目录下,它是DiskLruCache能够正常工作的前提,我们不需要频繁调用,一般只需在onPause()时调用即可。

接下来是读取缓存文件,具体步骤是:

1.通过DiskLruCache对象获取DiskLruCache.Snapshot对象,要读取缓存必须要用到这个snapshot对象:

DiskLruCache.Snapshot snapshot = mDiskLruCache.get(cacheKey);

2.通过snapshot获取输入流:

InputStream is = snapshot.getInputStream(0);

这里参数传0也是因为创建DiskLruCache时valueCount我们传了1。

3.得到输入流以后就可以做业务相关的操作了,比如解析出bitmap:

Bitmap bitmap = BitmapFactory.decodeStream(is);

完整demo如下:

public class MainActivity extends ActionBarActivity {

    private final String downloadUrl = "http://img3.douban.com/view/note/large/public/p28933592.jpg";

    private DiskLruCache mDiskLruCache;
    private ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mImageView = (ImageView) findViewById(R.id.iv);

        //打开硬盘缓存,最大容量设为10M
        final File cacheDir = getDiskCacheDir("bitmap");
        if(!cacheDir.exists()){
            cacheDir.mkdirs();
        }
        final int appVersion = getAppVersion();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mDiskLruCache = DiskLruCache.open(cacheDir, appVersion, 1, 10 * 1024 * 1024);

                    String cacheKey = getCacheKey(downloadUrl);
                    DiskLruCache.Editor editor = mDiskLruCache.edit(cacheKey);
                    if(editor != null){
                        OutputStream os = editor.newOutputStream(0);
                        //从网络上下载一张图片
                        if(downloadBitmap(downloadUrl, os)){
                            //将下载的图片存入到缓存中
                            editor.commit();
                        }else{
                            editor.abort();
                        }
                    }
                    mDiskLruCache.flush();

                    //从缓存中读取该图片并显示在ImageView中
                    DiskLruCache.Snapshot snapshot = mDiskLruCache.get(cacheKey);
                    if(snapshot != null){
                        InputStream is = snapshot.getInputStream(0);
                        final Bitmap bitmap = BitmapFactory.decodeStream(is);
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                mImageView.setImageBitmap(bitmap);
                            }
                        });
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private String getCacheKey(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    private File getDiskCacheDir(String folderName) {
        String cachePath;
        if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) && !Environment.isExternalStorageRemovable()){
            cachePath = getExternalCacheDir().getPath();
        }else{
            cachePath = getCacheDir().getPath();
        }
        return new File(cachePath, folderName);
    }

    private int getAppVersion() {
        try {
            PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    private boolean downloadBitmap(String urlString, OutputStream os) {
        try {
            URL url = new URL(urlString);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            BufferedInputStream bis = new BufferedInputStream(conn.getInputStream(), 4 * 1024);
            BufferedOutputStream bos = new BufferedOutputStream(os, 4 * 1024);
            int len;
            while((len = bis.read()) != -1){
                bos.write(len);
            }
            bis.close();
            bos.close();
            conn.disconnect();
            return true;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
}

这个demo中缓存的key没有直接用url,而是用了url的MD5编码,因为url中可能包含特殊字符,不能作为缓存文件的命名,而MD5编码既唯一又肯定符合命名要求。

另外,除了上面的存取、获取,还有移除操作,需要调用

mDiskLruCache.remove(key);  

这个通常不需要我们操作,因为缓存容量超过maxSize后,DiskLruCache会根据LRU算法自动删除某些缓存,所以除非你很明确某个缓存是没有必要的了,否则不必手动去调。

close()方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。关闭掉以后就不能再调用DiskLruCache中任何操作缓存数据的方法,通常只应该在Activity的onDestroy()方法中去调用close()方法。

delete()方法用于将所有的缓存数据全部删除,比如某些app设置里通常都有的手动清理缓存功能,其实只需要调用一下DiskLruCache的delete()方法就可以实现了。

journal文件简单介绍

这里写图片描述
由于现在只缓存了一张图片,所以journal中并没有几行日志,第一行是个固定的字符串“libcore.io.DiskLruCache”,标志着使用了DiskLruCache。第二行是DiskLruCache的版本号,这个值是恒为1的。第三行是应用程序的版本号,我们在open()方法里传入的版本号是什么这里就会显示什么。第四行是valueCount,这个值也是在open()方法中传入的,通常情况下都为1。第五行是一个空行。前五行也被称为journal文件的头,这部分内容还是比较好理解的,但是接下来的部分就要稍微动点脑筋了。

第六行是以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。

除了DIRTY、CLEAN、REMOVE之外,还有一种前缀是READ的记录,这个就非常简单了,每当我们调用get()方法去读取一条缓存数据时,就会向journal文件中写入一条READ记录。因此,非常大的程序journal文件中就可能会有大量的READ记录,那么你可能会担心了,如果我不停频繁操作的话,就会不断地向journal文件中写入数据,那这样journal文件岂不是会越来越大?这倒不必担心,DiskLruCache中使用了一个redundantOpCount变量来记录用户操作的次数,每执行一次写入、读取或移除缓存的操作,这个变量值都会加1,当变量值达到2000的时候就会触发重构journal的事件,这时会自动把journal中一些多余的、不必要的记录全部清除掉,保证journal文件的大小始终保持在一个合理的范围内。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LRU Cache (最近最少使用缓存) 和 DiskLruCache (基于磁盘的 LRU 缓存) 是两种常见的缓存技术,它们的使用场景和优点如下: 1. LRU Cache 的使用场景: - 当需要缓存一些数据时,但是又不能无限制地增加内存消耗时,可以使用 LRU Cache 进行缓存。 - 当需要快速访问某些数据时,而这些数据的访问频率比较高时,可以使用 LRU Cache 进行缓存。 - 当需要保证缓存数据的时效性,避免过期数据对程序造成影响时,可以使用 LRU Cache 进行缓存。 2. DiskLruCache 的使用场景: - 当需要缓存一些大量的数据时,但是这些数据又不能全部存放在内存中时,可以使用 DiskLruCache 进行缓存。 - 当需要保证数据能够持久化存储时,可以使用 DiskLruCache 进行缓存。 - 当需要对缓存数据进行一些额外的操作时,例如压缩、加密等操作时,可以使用 DiskLruCache 进行缓存。 以下是使用 Kotlin 代码展示 LRU CacheDiskLruCache 的实现方法: ```kotlin // LRU Cache 的实现 import android.util.LruCache // 初始化一个 LRU Cache,设置最大缓存数量为 10 个 val lruCache = LruCache<String, String>(10) // 将数据加入缓存lruCache.put("key1", "value1") // 获取缓存中的数据 val value = lruCache.get("key1") // 移除缓存中的数据 lruCache.remove("key1") // 清除缓存中的所有数据 lruCache.evictAll() ``` ```kotlin // DiskLruCache 的实现 import com.jakewharton.disklrucache.DiskLruCache import java.io.File // 初始化一个 DiskLruCache,设置缓存目录和最大缓存数量为 10 个 val directory = File(context.cacheDir, "disk_cache") val diskCacheSize = 10 * 1024 * 1024 // 10MB val diskLruCache = DiskLruCache.open(directory, 1, 1, diskCacheSize.toLong()) // 将数据加入缓存中 val editor = diskLruCache.edit("key1") editor?.newOutputStream(0)?.use { outputStream -> outputStream.write("value1".toByteArray()) } editor?.commit() // 获取缓存中的数据 val snapshot = diskLruCache.get("key1") val value = snapshot?.getInputStream(0)?.bufferedReader().use { reader -> reader?.readText() } // 移除缓存中的数据 diskLruCache.remove("key1") // 清除缓存中的所有数据 diskLruCache.delete() ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值