源码分析 LruCache

2018-07-29  本文已影响0人  Parallel_Lines

简介

为什么用

一个app持有的内存是有限的,无限制的使用强引用在内存中缓存数据,有可能导致OOM。

能做什么

  1. LruCache会创建一个固定大小的缓存池,并维持一个LinkHashMap来有序的缓存数据。
  2. 在往缓存池put或get数据的时候,LinkHashMap会将最近使用的数据移动到队尾。
  3. 在往缓存池put数据的时候,LruCache会计算当前已缓存数据的大小。如果当前缓存数据超过了限定值,LruCache会将LinkHashMap队首的数据删除,直到缓存数据大小满足限定值。

即缓存一定量的数据,并在缓存数据超限时删除长期不用的数据。

如何使用

以缓存Bitmap为例:


int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(cacheSize) {

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }
};

需要缓存图片时,直接lruCache.put(key, bitmap);
需要获取图片时,三级缓存,先从内存拿,故调用lruCache.get从内存缓存拿。

源码分析

前提知识

LinkHashMap可以将最近使用的元素移动至队尾。见构造函数:


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

//LruCache中如下初始化LinkHashMap
new LinkedHashMap<K, V>(0, 0.75f, true)

第三个参数:accessOrder为true时,在LinkHashMap调用get时,会执行排序:

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

排序会将get的元素移动到LinkHashMap的队尾:

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMapEntry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

LruCache源码分析

构造函数

首先是构造函数,LruCache的构造函数指定了缓存的maxSize,以及创建了一个get会自动排序的LinkHashMap。

    public LruCache(int maxSize) {
        ...
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

这里需要注意的是:maxSize是可定制的,不一定是占用内存的大小,也可以是元素的数量(数量也可以限制缓存的大小)。
如何鉴别maxSize的意义呢?
上面使用LruCache的例子中,重写了一个方法sizeOf:

LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(cacheSize) {

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }
};

sizeOf是返回当前元素占用maxSize的大小(不是比例),它的默认方法如下:

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

默认返回1,表示占用一个元素。可见LruCache的maxSize的默认意义是可容纳元素的数量
上述例子中,重写了sizeOf,使其返回bitmap的大小,故maxSize的意义也应当变更为可缓存数据的内存大小。

Put

分析完构造函数,接下来就是LruCache的功能函数:put和get
先存后取,先分析put。put的源码如下:

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

    V previous;
    synchronized (this) {
        ...
        size += safeSizeOf(key, value);
        previous = map.put(key, value);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }

    trimToSize(maxSize);
    return previous;
}

这个方法做了三件事:

1.将数据存储到LinkHashMap里。previous是该key对应的旧value,没有则返回null。

previous = map.put(key, value);

2.重新计算当前size。size = size + 新value_size - 旧value_size。这里加了同步,防止多线程size计算错误。

size += safeSizeOf(key, value);
...
if (previous != null) {
    size -= safeSizeOf(key, previous);
}

safeSizeOf()就是上面重写的sizeOf(),只不过加了一层size<0的异常处理。

3.计算当前size<maxSize?,不满足则删除LinkHashMap队首元素,删除后重新计算size直到<maxSize。

trimToSize(maxSize);

trimToSize(maxSize)的源码如下:

    public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                ...

                //size<maxSize,退出删除循环;否则继续删除
                if (size <= maxSize) {
                    break;
                }

                //获取队首元素,也就是最近最少使用的元素
                Map.Entry<K, V> toEvict = map.eldest();
                ...

                //remove队首元素,并重新计算size
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                ...
            }

            entryRemoved(true, key, value, null);
        }
    }

参考注释

无论是put(),还是trimToSize(),似乎都有调用一个方法entryRemoved()
entryRemove()是LruCache的一个空方法,设置它的意义在于:

LruCache之所以能缓存,其实就是利用LinkHashMap对数据强引用。删除缓存,实际就是解除强引用。但是对于有些数据,强引用的解除并不能真正的回收。如旧版本的Android,对于Bitmap的回收必须手动调用recycle()。这个方法,就是提供给外部,以满足类似这种额外的内存回收需求。

Get

Get的源码如下:

    public final V get(K key) {
        ...

        //获取数据
        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);
            if (mapValue != null) {
                ...
                return mapValue;
            }
            ...
        }

        //获取不到,尝试create数据
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

这个方法做了俩件事:

1.将数据从LinkHashMap中取出并返回。(排序是LinkHashMap在get时调用的,LruCache无须关心)

mapValue = map.get(key);
if (mapValue != null) {
    ...
    return mapValue;
}

2.如果获取不到数据,尝试create。create的默认方法如下:

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

同样,create()类似于entryRemoved(),也是提供给外部,供其在获取不到数据下提供默认数据的方法。

下节将分析DiskLruCache。

上一篇 下一篇

猜你喜欢

热点阅读