美文网首页
源码分析 LruCache

源码分析 LruCache

作者: Parallel_Lines | 来源:发表于2018-07-29 20:10 被阅读0次

    简介

    为什么用

    一个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。

    相关文章

      网友评论

          本文标题:源码分析 LruCache

          本文链接:https://www.haomeiwen.com/subject/mfstvftx.html