Android缓存策略

前言

最近在刷面试题,遇到一个问题,关于缓存的原理的,所以在这里几个笔记,关于缓存很多大牛都说过了,我只是做个笔记,下面的很多都是网上查看到的,并非原创

目录

  • 一:Android 缓存策略
      1. 内存缓存(LruCache)
    • 2.磁盘缓存(文件缓存)——DiskLruCache分析
    • ASimpleCache
  • 二:使用
      1. LRU使用
      1. DiskLruCache
  • 三 源码分析
      1. LRU 源码分析
      1. DiskLruCache分析

一:Android 缓存策略

1. 内存缓存(LruCache)

LRU,全称Least Rencetly Used,即最近最少使用,是一种非常常用的置换算法,也即淘汰最长时间未使用的对象。LRU在操作系统中的页面置换算法中广泛使用,我们的内存或缓存空间是有限的,当新加入一个对象时,造成我们的缓存空间不足了,此时就需要根据某种算法对缓存中原有数据进行淘汰货删除,而LRU选择的是将最长时间未使用的对象进行淘汰。

2. 磁盘缓存(文件缓存)——DiskLruCache分析

JakeWharton/DiskLruCache

不同于LruCache,LruCache是将数据缓存到内存中去,而DiskLruCache是外部缓存,例如可以将网络下载的图片永久的缓存到手机外部存储中去,并可以将缓存数据取出来使用,DiskLruCache不是google官方所写,但是得到了官方推荐

3. ASimpleCache

ASimpleCache如何使用


二:使用

1. LRU使用

(1) 实例化

看下源码

public class LruCache<K, V> {}

键值对的形式

下面是往上很多标准的写法,用于保存图片

    private void init_Lru() {
        //设置LruCache缓存的大小,一般为当前进程可用容量的1/8。
        int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
        int cacheSize = maxMemory / 8;
        // 重写sizeOf方法,计算出要缓存的每张图片的大小。
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // 重写sizeOf方法,计算出要缓存的每张图片的大小。
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };
}

(2)保存

 public final V put(K key, V value) {}

(3)获取

  public final V get(K key) {}

2. DiskLruCache

(1) 权限

因为要操作外部存储,所以必须要先加上权限:

<!-- 在SDCard中创建与删除文件权限 -->  
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>  
<!-- 往SDCard写入数据权限 -->  
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 

另外要从网络下载图片,还要加上权限:

 <uses-permission android:name="android.permission.INTERNET" />

(2)初始化DiskLruCache

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

参数说明

  • File directory 数据的缓存地址
  • int appVersion 当前应用程序的版本号
  • int valueCount 指定同一个key可以对应多少个缓存文件,基本都是传1
  • long maxSize 定最多可以缓存多少字节的数据

获取缓存地址的方法

    /***
     *
     * @param context
     * @param uniqueName 缓存地址的名字
     * @return 缓存地址
     */
    public File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        //当SD卡存在或者SD卡不可被移除
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            // 路径/sdcard/Android/data/<application package>/cache/uniqueName
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            // 路径/data/data/<application package>/cache/uniqueName
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

获取App版本

    /***
     * 
     * @param context
     * @return App 版本
     */
    public int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

初始化

        File test = getDiskCacheDir(this, "Bitmap");
        int appVersion = getAppVersion(this);
        long maxSize = 10 * 1024 * 1024;
        try {
            mDiskLruCache = DiskLruCache.open(test, appVersion, 1, maxSize);
        } catch (IOException e) {
            e.printStackTrace();
        }

(3) 存数据

String key = hashKeyForDisk(url);  
DiskLruCache.Editor editor = mDiskLruCache.edit(key); 
OuputStream os = editor.newOutputStream(0); 


//进行提交才能使写入生效
 editor.commit();
//表示放弃此次写入
 editor.abort();

生成MD5


    public String hashKeyForDisk(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();
    }

(4)取数据

 DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
if (snapShot != null) {  
    InputStream is = snapShot.getInputStream(0);  
}

(5)删除数据

public synchronized boolean remove(String key) throws IOException

(6)其他API

  • size()
    这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来
  • flush()
    这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候我有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了
  • close()
    这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法,通常只应该在Activity的onDestroy()方法中去调用close()方法。
  • delete()
    这个方法用于将所有的缓存数据全部删除,比如说网易新闻中的那个手动清理缓存功能,其实只需要调用一下DiskLruCache的delete()方法就可以实现了。

三. 源码分析

1. LRU 源码分析

属性

public class LruCache<K, V> {

private int size;// 当前大小
private int maxSize;// 最大容量

private int putCount;// put次数
private int createCount;// 创建次数
private int evictionCount;// 回收次数
private int hitCount;// 命中次数
private int missCount;// 未命中次数
}

构造方法

    /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
  • 设置了最大容量
  • 可以看到有一个LinkedHashMap,这个是核心,用于储存缓存的对象,
  • 在LinkedHashMap中的第三个参数指定为true,表示这个LinkedHashMap将是基于数据的访问顺序进行排序。为false,则为插入顺序(这里就是核心)

sizeOf && safeSizeOf

 private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }

    /**
     * Returns the size of the entry for {@code key} and {@code value} in
     * user-defined units.  The default implementation returns 1 so that size
     * is the number of entries and max size is the maximum number of entries.
     *
     * <p>An entry's size must not change while it is in the cache.
     */
    protected int sizeOf(K key, V value) {
        return 1;
    }
  • 由于各种数据类型大小测量的标准不统一,具体测量的方法应该由使用者来实现

保存数据

下面代码来自 http://blog.csdn.net/shakespeare001/article/details/51695358

/**
   * 给对应key缓存value,并且将该value移动到链表的尾部。
   */
public final V put(K key, V value) {
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

      V previous;
      synchronized (this) {
        // 记录 put 的次数
        putCount++;
        // 通过键值对,计算出要保存对象value的大小,并更新当前缓存大小
        size += safeSizeOf(key, value);
        /*
         * 如果 之前存在key,用新的value覆盖原来的数据, 并返回 之前key 的value
         * 记录在 previous
         */
        previous = map.put(key, value);
        // 如果之前存在key,并且之前的value不为null
        if (previous != null) {
            // 计算出 之前value的大小,因为前面size已经加上了新的value数据的大小,此时,需要再次更新size,减去原来value的大小
            size -= safeSizeOf(key, previous);
        }
      }

    // 如果之前存在key,并且之前的value不为null
    if (previous != null) {
        /*
         * previous值被剔除了,此次添加的 value 已经作为key的 新值
         * 告诉 自定义 的 entryRemoved 方法
         */
        entryRemoved(false, key, previous, value);
    }
    //裁剪缓存容量(在当前缓存数据大小超过了总容量maxSize时,才会真正去执行LRU)
    trimToSize(maxSize);
      return previous;
}
  • key和value判空,说明LruCache中不允许key和value为null;
  • 通过safeSizeOf()获取要加入对象数据的大小,并更新当前缓存数据的大小;
  • 将新的对象数据放入到缓存中,即调用LinkedHashMap的put方法,如果原来存在该key时,直接替换掉原来的value值,并返回之前的value值,得到之前value的大小,更新当前缓存数据的size大小;如果原来不存在该key,则直接加入缓存即可;
  • 清理缓存空间,当我们加入一个数据时(put),为了保证当前数据的缓存所占大小没有超过我们指定的总大小,通过调用trimToSize()来对缓存空间进行管理控制。

trimToSize

public void trimToSize(int maxSize) {
    /*
     * 循环进行LRU,直到当前所占容量大小没有超过指定的总容量大小
     */
    while (true) {
        K key;
        V value;
        synchronized (this) {
            // 一些异常情况的处理
            if (size < 0 || (map.isEmpty() && size != 0)) {
                throw new IllegalStateException(
                        getClass().getName() + ".sizeOf() is reporting inconsistent results!");
            }
            // 首先判断当前缓存数据大小是否超过了指定的缓存空间总大小。如果没有超过,即缓存中还可以存入数据,直接跳出循环,清理完毕
            if (size <= maxSize || map.isEmpty()) {
                break;
            }
            /**
             * 执行到这,表示当前缓存数据已超过了总容量,需要执行LRU,即将最近最少使用的数据清除掉,直到数据所占缓存空间没有超标;
             * 根据前面的原理分析,知道,在链表中,链表的头结点是最近最少使用的数据,因此,最先清除掉链表前面的结点
             */
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            // 移除掉后,更新当前数据缓存的大小
            size -= safeSizeOf(key, value);
            // 更新移除的结点数量
            evictionCount++;
        }
        /*
         * 通知某个结点被移除,类似于回调
         */
        entryRemoved(true, key, value, null);
    }
}
  • trimToSize()方法的作用就是为了保证当前数据的缓存大小不能超过我们指定的缓存总大小,如果超过了,就会开始移除最近最少使用的数据,直到size符合要求。
  • trimToSize()方法在put()的时候一定会调用,在get()的时候有可能会调用。

public final V get(K key) {}获取数据

/**
 * 根据key查询缓存,如果该key对应的value存在于缓存,直接返回value;
* 访问到这个结点时,LinkHashMap会将它移动到双向循环链表的的尾部。
* 如果如果没有缓存的值,则返回null。(如果开发者重写了create()的话,返回创建的value)
*/
public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        // LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
        mapValue = map.get(key);
        // 计算 命中次数
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        // 计算 丢失次数
        missCount++;
    }

    /*
     * 官方解释:
     * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
     * 候,用这个key执行了put方法,那么此时就发生了冲突,我们在Map中删除这个创建的值,释放被创建的值,保留put进去的值。
     */
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    /***************************
     * 不覆写create方法走不到下面 *
     ***************************/
    /*
     * 正常情况走不到这里
     * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
     * 因为默认的 create(K key) 逻辑为null
     */
    synchronized (this) {
        // 记录 create 的次数
        createCount++;
        // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
        mapValue = map.put(key, createdValue);

        // 如果之前存在相同key的value,即有冲突。
        if (mapValue != null) {
            /*
             * 有冲突
             * 所以 撤销 刚才的 操作
             * 将 之前相同key 的值 重新放回去
             */
            map.put(key, mapValue);
        } else {
            // 拿到键值对,计算出在容量中的相对长度,然后加上
            size += safeSizeOf(key, createdValue);
        }
    }

    // 如果上面 判断出了 将要放入的值发生冲突
    if (mapValue != null) {
        /*
         * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
         * 告诉 自定义 的 entryRemoved 方法
         */
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        // 上面 进行了 size += 操作 所以这里要重整长度
        trimToSize(maxSize);
        return createdValue;
    }
}
  • 先尝试从map缓存中获取value,即mapVaule = map.get(key);如果mapVaule != null,说明缓存中存在该对象,直接返回即可;
  • 如果mapVaule == null,说明缓存中不存在该对象,大多数情况下会直接返回null;但是如果我们重写了create()方法,在缓存没有该数据的时候自己去创建一个,则会继续往下走,中间可能会出现冲突,看注释;
  • 注意:在我们通过LinkedHashMap进行get(key)或put(key,value)时都会对链表进行调整,即将刚刚访问get或加入put的结点放入到链表尾部。

entryRemoved()

/**
* 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用
* 或者替换条目值时put调用,默认实现什么都没做。
* 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。
* 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove)  evicted=false:put冲突后 或 get里成功create后
* 导致
* 4.newValue!=null,那么则被put()或get()调用。
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}
  • 可以发现entryRemoved方法是一个空方法,说明这个也是让开发者自己根据需求去重写的。
  • entryRemoved()主要作用就是在结点数据value需要被删除或回收的时候,给开发者的回调。开发者就可以在这个方法里面实现一些自己的逻辑:
    • 可以进行资源的回收;
    • 可以实现二级内存缓存,可以进一步提高性能,思路如下:

二级缓存

  • 重写LruCache的entryRemoved()函数,
  • 把删除掉的item,再次存入另外一个LinkedHashMap<String, SoftWeakReference<Bitmap>>
  • 这个数据结构当做二级缓存,每次获得图片的时候,先判断LruCache中是否缓存,没有的话,再判断这个二级缓存中是否有,如果都没有再从sdcard上获取。sdcard上也没有的话,就从网络服务器上拉取。

entryRemoved()在LruCache中有四个地方进行了调用:put()、get()、trimToSize()、remove()中进行了调用。


2. DiskLruCache

Android DiskLruCache 源码解析 硬盘缓存的绝佳方案


原文地址

http://blog.csdn.net/shakespeare001/article/details/51695358

https://www.tuicool.com/articles/JB7RNj