Android 缓存 - LruCache

在开发Android过程中,多少会涉及到缓存。例如加载网络图片,不能每次要显示某张网络图片,都要从网络下载,这样的话,不仅浪费用户流量,还可能会造成不好的体验。一般的做法都是先将图片加载到本地保存起来,下次还需要显示同一张图片,直接从内存中获取就行,无需通过网络。这就是缓存了。

我们都知道,Android的内存少的可怜,动不动就OOM,所以缓存需要一定的策略,如LRU,LFU,FOFI。

LRU(Least Recently Used),最近最少使用原则,也就是淘汰最长时间未使用的对象。如缓存的顺序为
A -> B -> C -> D -> A
假如已缓存满了,此时要假如一个E,根据最近最少使用原则,则会淘汰B,那么容器中只剩下ACDE。

我们先看一下怎么使用这个LruCache缓存数据:

int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache bitmapCache = new LruCache(cacheSize) {
    protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount();
    }
}}

从源码分析LruCache,从属性看起:

// 保存数据的Map
private final LinkedHashMap map;
// 当前size,不一定是个数,从上面的例子可以看出,我们可以自定义size
private int size;
// 最大size,同上
private int maxSize;
// put个数
private int putCount;
// get没击中后,创建个数
private int createCount;
// 剔除的个数
private int evictionCount;
// get击中(根据key找到value)个数
private int hitCount;
// get没击中个数
private int missCount;

LruCache使用了LinkedHashMap保存数据。LinkedHashMap有个特性,就是保存的数据按访问顺序来排列的,举个例子就很清楚了:

public class Tester {
    private static int MAX=5;
    public static void main(String[] args) {
        HashMap map = new LinkedHashMap(MAX/2,0.5f,true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest){
                // 如果超过了MAX个,就允许删除最老一个
                if(size() > MAX) {
                    return true;
                }
                return false;
            }
        };
        map.put("a", "a");
        map.put("b", "b");
        map.put("c", "c");
        map.put("d", "d");
        map.put("e", "e");
        map.put("c", "c");
        for (Map.Entry entry : map.entrySet()) {  
            System.out.print(entry.getValue() + ", ");  
        }  
        System.out.println();
        map.get("b");
        for (Map.Entry entry : map.entrySet()) {  
            System.out.print(entry.getValue() + ", ");  
        }  
        System.out.println();
        map.put("f", "f");
        for (Map.Entry entry : map.entrySet()) {  
            System.out.print(entry.getValue() + ", ");  
        }  
        System.out.println();
    }
}

打印结果为:

a, b, d, e, c, 
a, d, e, c, b, 
d, e, c, b, f,

LruCache正好利用了LinkedHashMap这个特性做到最近最少使用原则的。再看构造函数:

public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap(0, 0.75f, true);
}

设置了最大size,并初始化了LinkedHashMap,我们结合一下LinkedHashMap构造函数了解初始化参数:

public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {  
    super(initialCapacity, loadFactor);  
    init();  
    this.accessOrder = accessOrder;  
}  

initialCapacity是初始化空间大小
loadFactor是加载因子,就是当他的size大于initialCapacity*loadFactor的时候就会扩容
accessOrder是否按照访问顺序排列数据

LruCache的put方法是缓存数据的,代码如下:

public final V put(K key, V value) {
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }
    V previous;
    synchronized (this) {
        putCount++;
        // size增加新值大小
        size += safeSizeOf(key, value);
        previous = map.put(key, value);
        if (previous != null) {
            // 如果之前有值,size减去旧值大小
            size -= safeSizeOf(key, previous);
        }
    }
    if (previous != null) {
        // 提供子类释放旧的资源
        entryRemoved(false, key, previous, value);
    }
    // 限制大小
    trimToSize(maxSize);
    return previous;
}

put方法先调用了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;
}

safeSizeOf调用了sizeOf方法计算:

protected int sizeOf(K key, V value) {
   return 1;
}

sizeOf方法默认返回1,也就是LruCache默认计算大小的方式是,我们put一个数据,size加1,所以一般我们会重写这个方法,如:

int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache bitmapCache = new LruCache(cacheSize) {
    protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount();
    }
}}

我们继续看put方法,计算完大小之后,然后调用map.put(key, value)方法把数据加入到LinkedHashMap中。如果之前有数据,也就是previous不为null,size要减去previous的大小。如果previous不为null,还调用了entryRemoved方法:

protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

竟然是一个空的方法,也就是子类可以重写这个方法。根据官方解释,如果你的缓存值,需要显式释放的资源,你可以调用这个方法。明白了吧!!!继续put方法,最后调用了trimToSize方法:

public void trimToSize(int maxSize) {
    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) {
                break;
            }
            // 如果当前size大于最大size,取出最老(不常)的一个
            Map.Entry toEvict = map.eldest();
            if (toEvict == null) {
                break;
            }
            key = toEvict.getKey();
            value = toEvict.getValue();
            // 从LinkedHashMap移除
            map.remove(key);
            // 减去移除数据的大小
            size -= safeSizeOf(key, value);
            evictionCount++;
        }
        entryRemoved(true, key, value, null);
    }
}

从代码注释的分析来看,trimToSize方法是限制大小,如果当前size超过最大的size,剔除最近最不常用的数据。put方法分析完毕,一般的套路都是,有put就是get,我们看看get方法:

public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }
    V mapValue;
    synchronized (this) {
        mapValue = map.get(key);
        // 找到直接返回
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }
    /*
     * Attempt to create a value. This may take a long time, and the map
     * may be different when create() returns. If a conflicting value was
     * added to the map while create() was working, we leave that value in
     * the map and release the created value.
     */
    // 尝试根据key创建新的value
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }
    synchronized (this) {
        createCount++;
        // 加入后,判断是否有旧的值
        mapValue = map.put(key, createdValue);
        if (mapValue != null) {
            // There was a conflict so undo that last put
            // 把旧的值放回去
            map.put(key, mapValue);
        } else {
            // size增加新创建的值大小
            size += safeSizeOf(key, createdValue);
        }
    }
    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        // 返回查找的值
        return mapValue;
    } else {
        // 限制大小
        trimToSize(maxSize);
        // 返回新创建的值
        return createdValue;
    }
}

先从map中根据key找value,如果value不为空,直接返回value,结束查找。如果没找到,调用create(key)方法尝试创建一个,如果不为null,就保存到map中:

protected V create(K key) {
    return null;
}

默认返回一个null对象,所以这个方法和entryRemoved方法一样,提供子类重写,意思是,如果我们获取不到key对应的值,重写之后,可以提供一个默认的值,并保存到map中。保存的过程中,会涉及到线程不同问题,还要判断一下,是不是已经有值了(如果你觉得:就是因为找不到value,才创建新的,为何还要判断map的key是否有值,那证明你线程同步还得学习一下)。如果有值,就不把新创建的加入到map中,并返回该值。如果没有值,就把心创建的加入map中,调用trimToSize重新限制大小,并返回新创建的值。

LruCache提供了remove方法删除缓存,代码:

public final V remove(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }
    V previous;
    synchronized (this) {
        previous = map.remove(key);
        // 如果删除成功,size减去移除数据的大小
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }
    // 提供子类释放资源
    if (previous != null) {
        entryRemoved(false, key, previous, null);
    }
 // 返回当前移除的对象
    return previous;
}

还提供了清除所有缓存的方法:

public final void evictAll() {
    trimToSize(-1); // -1 will evict 0-sized elements
}

LruCache允许调用resize方法重新定义最大size:

public void resize(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    synchronized (this) {
        this.maxSize = maxSize;
    }
    trimToSize(maxSize);
}

LruCache源码并不多,也比较容易理解。

你可能感兴趣的:(Android,缓存,源码分析)