Android内存缓存管理LruCache源码解析与示例

Posted 单灿灿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android内存缓存管理LruCache源码解析与示例相关的知识,希望对你有一定的参考价值。

在深圳出差,非常忙,抽空写文章,这些文章的质量很可能不高,但还是希望可以帮到你。

在加载图片的时候,我们要考虑到内存问题,(内存缓存作为最先被读取的数据,应该存储那些经常使用的数据对象,且内存容量有限,内存缓存的容量应该限定。)如果你加载是高清无码大图很可能会造成OOM,那我们需要一个东西来管理这个图片与其缓存。

今天我们来讲一下LruCache的原理及实现,这个谷歌推荐的内存缓存的方法。

那么Lru是什么?

LRU全称为Least Recently Used,即最近最少使用,是一种缓存置换算法。(多加一句LFU(least frequently used )算法,则淘汰的是最不经常使用的)。

问题:当有新的内容需要加入我们的缓存,但我们的缓存空闲的空间不足以放进新的内容时,如何舍弃原有的部分内容从而腾出空间用来放新的内容。

让我们进入一下LruCache的学习

首先让我们看一下使用方法中的初始化MemoryCache:

public class BitmapMemoryCache 

    private static final String TAG = "BitmapMemoryCache";

    private static BitmapMemoryCache sInstance = new BitmapMemoryCache();

    private LruCache<String, Bitmap> mMemoryCache;


    public Map<String, SoftReference<Bitmap>> mImageCacheMap = new HashMap<String, SoftReference<Bitmap>>();

    /*单例模式*/
    public  BitmapMemoryCache getInstance() 
        return BitmapMemoryCache.sInstance;
    

    private BitmapMemoryCache() 
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//获取系统分配给应用的总内存大小,不是获取系统全部的的
        int cacheSize = maxMemory / 8;//设置图片内存缓存占用八分之一,要依据你申请下来的和你估算使用的大小来
        Log.e(TAG, "" + cacheSize);
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) 
            @Override
            protected int sizeOf(String key, Bitmap bitmap) 
                // 重写此方法来衡量每张图片的大小,默认返回图片数量。
                Log.w(TAG, "addBitmapTo " + (bitmap.getByteCount() / 1024));
                return bitmap.getByteCount() / 1024;
            
        ;
    
  

接着让我们进入LruCache源码

明显的可以看出LruCache是利用了LinkedHashMap

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

    /** Size of this cache in units. Not necessarily the number of elements. */
    private int size;//当前缓存内容的大小。
    private int maxSize; // 最大可缓存的大小

    private int putCount;// put方法被调用的次数
    private int createCount;//create(Object) 被调用的次数
    private int evictionCount;//被置换出来的元素的个数
    private int hitCount; //get方法获取到缓存中的元素的次数
    private int missCount;//get方法未获取到缓存中元素的次数

    /**
     * @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);
    

    /**
     * Sets the size of the cache.
     *//设置缓存大小
     * @param maxSize The new maximum size.
     */
    public void resize(int maxSize) 
        if (maxSize <= 0) 
            throw new IllegalArgumentException("maxSize <= 0");
        

        synchronized (this) 
            this.maxSize = maxSize;
        
        trimToSize(maxSize);
    
  

走到这里我们有个疑问—LinkedHashMap是什么?它是怎么实现LRU这种缓存策略的?

看文中的一句代码:

this.map = new LinkedHashMap<K, V>(0, 0.75f, true);

让我们进入LinkedHashMap源码来看一下。

进入构造方法查看。

 /**

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

参数说明:

  • initialCapacity 初始容量大小,使用无参构造方法时,此值默认是4(安卓SdkVersion 24中默认4,这里是使用了父类HashMap的默认值)
  • loadFactor 加载因子,使用无参构造方法时,此值默认是 0.75f(安卓SdkVersion 24中默认0.75,这里是使用了父类HashMap的默认值)
  • accessOrder false: 基于插入顺序 true: 基于访问顺序

LinkedHashMap继承自HashMap,不同的是,它是一个双向循环链表,它的每一个数据结点都有两个指针,分别指向直接前驱和直接后继,这一个我们可以从它的内部类LinkedEntry中看出,其定义如下:

 /**
     private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> 
        // These fields comprise the doubly linked list used for iteration.
        LinkedHashMapEntry<K,V> before, after;
    //一个双向循环链表,它的每一个数据结点都有两个指针,
    //分别指向直接前驱和直接后继

        LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<K,V> next) 
            super(hash, key, value, next);
        

        private void remove() 
            before.after = after;
            after.before = before;
        


      // Inserts this entry before the specified existing entry in the list.

        private void addBefore(LinkedHashMapEntry<K,V> existingEntry) 
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        


        void recordAccess(HashMap<K,V> m) 
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) 
                lm.modCount++;
                remove();
                addBefore(lm.header);
            
        

        void recordRemoval(HashMap<K,V> m) 
            remove();
        
    

LinkedHashMap实现了双向循环链表的数据结构。

  • 1,当链表不为空时,header.after指向第一个结点,header.before指向最后一个结点;

  • 2,当链表为空时,header.after与header.before都指向它本身。

    @Override

    void init()

    header = new LinkedHashMapEntry<>(-1, null, null,null);

    header.before = header.after = header;

  • accessOrder是指定它的排序方式,当它为false时,只按插入的顺序排序,即新放入的顺序会在链表的尾部;而当它为true时,更新或访问某个节点的数据时,这个对应的结点也会被放到尾部。

  • 它通过构造方法public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)来赋值。

我们加入一个新结点来看一下方法执行过程(在LinkedHashMap中):

   void addEntry(int hash, K key, V value, int bucketIndex) 
        // Previous android releases called removeEldestEntry() before actually
        // inserting a value but after increasing the size.
        // The RI is documented to call it afterwards.
        // **** THIS CHANGE WILL BE REVERTED IN A FUTURE ANDROID RELEASE ****

        //这个地方我专门去看了24和25,他并没有改,不知什么原因,欺骗我的感情

        // Remove eldest entry if instructed  如果得到通知,移除最旧的
        LinkedHashMapEntry<K,V> eldest = header.after;
        if (eldest != header) 
            boolean removeEldest;
            size++;
            try 
                removeEldest = removeEldestEntry(eldest);
             finally 
                size--;
            
            if (removeEldest) 
                removeEntryForKey(eldest.key);
            
        

        super.addEntry(hash, key, value, bucketIndex);//调用父类的添加方法
    

好,我们来看一下父类的添加方法:

想要理解HashMap可以看我的这篇HashMap源码解析


    //父类的这个方法也是将新添加的放入尾部,这里既是链表的尾部
 void addEntry(int hash, K key, V value, int bucketIndex) 
        if ((size >= threshold) && (null != table[bucketIndex])) 
            resize(2 * table.length);
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        

        createEntry(hash, key, value, bucketIndex);
    
    void createEntry(int hash, K key, V value, int bucketIndex) 
        HashMapEntry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
        size++;
    

当我们加入新的元素之后,链表的顺序如图:

那么当我们访问了或者是更新了某个元素(当accessOrder为true时),链表里的元素位置怎么变化呢?

让我们来看一下get(Object key)方法的流程:

 public V get(Object key) 
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);//获取LinkedHashMapEntry
        if (e == null)
            return null;
        e.recordAccess(this);//此方法记录下来,并重新排序
        return e.value;
    

进入recordAccess()查看:

这个方法是在LinkedHashMapEntry内部:

 //从这段代码中我们看到,首先执行remove,在执行addBefore
 LinkedHashMapEntry<K,V> before, after;

void recordAccess(HashMap<K,V> m) 
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) 
                lm.modCount++;
                remove();
                addBefore(lm.header);
            
        

 private void remove() //这是将此节点取出,如图一
            before.after = after;
            after.before = before;
        //这有点绕,按照我的图片来捋一遍
        //  node1.after=node3;
        //  node3.before=node1
        
  private void addBefore(LinkedHashMapEntry<K,V> existingEntry) //此处将header传入,将操作的节点放置链表末尾
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        

图一:

是不是理解了LinkedHashMap的排序原理了?

熟悉了LinkedHashMap,让我们来分析LruCache;

我们发现,通过它来实现Lru算法也就变得理所当然了。我们所需要做的,就只剩下定义缓存的最大大小,记录缓存当前大小,在放入新数据时检查是否超过最大大小。

所以LruCache定义了以下三个必需的成员变量:

   private final LinkedHashMap<K, V> map;

    /** Size of this cache in units. Not necessarily the number of elements. */
    private int size;//当前缓存内容的大小。
    private int maxSize; // 最大可缓存的大小

让我们来解析一下它的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++;//获取到缓存中的元素的次数+1,在文章头部有这几个参数的介绍
                return mapValue;
            
            missCount++;//未获取到缓存中的元素的次数+1
        

        V createdValue = create(key);
        if (createdValue == null) 
            return null;//如果没有为key创建新值成功,则直接返回null
        

        synchronized (this) 
            createCount++;
            mapValue = map.put(key, createdValue);
 //create调用次数+1  将创建的值放入map中,如果map在前面的过程中正好放入了这对key-value,那么会返回放入的value

            if (mapValue != null) 
                // There was a conflict so undo that last put
                map.put(key, mapValue);
        /如果不为null,说明不需要我们所创建的值,所以把返回的值放进去
             else 
                size += safeSizeOf(key, createdValue);
        //为null,说明我们更新了这个key的值,需要重新计算大小
            
        

       if (mapValue != null) //上面放入的值有冲突
            entryRemoved(false, key, createdValue, mapValue);// 移除之前创建的值,改为mapValue
            return mapValue;
         else 
//没有冲突时,因为放入了新创建的值,大小已经有变化,所以需要调整大小
            trimToSize(maxSize);
            return createdValue;
        
    

LruCache是可能被多个线程同时访问的,所以在读写map时进行加锁。

当获取不到对应的key的值时,它会调用其create(K key)方法,这个方法用于当缓存没有命名时计算一个key所对应的值,它的默认实现是直接返回null。

这个方法并没有加上同步锁,也就是在它进行创建时,map可能已经有了变化。

所以在get方法中,如果create(key)返回的V不为null,会再把它给放到map中,并检查是否在它创建的期间已经有其他对象也进行创建并放到map中了,

如果有,则会放弃这个创建的对象,而把之前的对象留下,否则因为我们放入了新创建的值,所以要计算现在的大小并进行trimToSize。

trimToSize方法是根据传进来的maxSize,如果当前大小超过了这个maxSize,则会移除最老的结点,直到不超过。

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 || map.isEmpty()) 
                    break;
                

                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);
        
    

接下来,我们再来看LruCach的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 += 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;
    

主要逻辑是,计算新增加的大小,加入size,

然后把key-value放入map中,如果是更新旧的数据(map.put(key, value)会返回之前的value),则减去旧数据的大小,并调用entryRemoved(false, key, previous, value)方法通知旧数据被更新为新的值。

最后也是调用trimToSize(maxSize)修整缓存的大小。

文末附上我以前写的一个管理类

/**
 * Bitmap缓存,简单缓存.
 * Created by ChangMingShan on 2015/12/26.
 */
public class BitmapMemoryCache 

    private static final String TAG = "BitmapMemoryCache";

    private static BitmapMemoryCache sInstance = new BitmapMemoryCache();

    private LruCache<String, Bitmap> mMemoryCache;

    /**
     * 单例模式.
     */
    public static BitmapMemoryCache getInstance() 
        return BitmapMemoryCache.sInstance;
    

    private BitmapMemoryCache() 
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) 
            @Override
            protected int sizeOf(String key, Bitmap bitmap) 
                // 重写此方法来衡量每张图片的大小,默认返回图片数量。
                return bitmap.getByteCount() / 1024;
            
        ;
    
    public synchronized void addBitmapToMemoryCache(String key, Bitmap bitmap) 
        if (mMemoryCache.get(key) == null) 
            if (key != null && bitmap != null)
                mMemoryCache.put(key, bitmap);
         else
            Log.w(TAG, "the res is aready exits");
    

    public synchronized Bitmap getBitmapFromMemCache(String key) 
        Bitmap bm = mMemoryCache.get(key);
        if (key != null) 
            return bm;
        
        return null;
    

    /**
     * 移除缓存
     *
     * @param key
     */
    public synchronized void removeImageCache(String key) 
        if (key != null) 
            if (mMemoryCache != null) 
                Bitmap bm = mMemoryCache.remove(key);
                if (bm != null)
                    bm.recycle();
            
        
    
     /**
     * 移除缓存
     */
    public synchronized void clearImageCache() 

        if (mMemoryCache != null) 
            if (mMemoryCache.size() > 0) 
                Log.d("CacheUtils",
                        "mMemoryCache.size() " + mMemoryCache.size());
                mMemoryCache.evictAll();
                Log.d("CacheUtils", "mMemoryCache.size()" + mMemoryCache.size());
            
            mMemoryCache = null;
        
    


    public Bitmap loadLocal(String path) 

        Bitmap bitmap=BitmapFactory.decodeFile(path);

        addBitmapToMemoryCache(path, bitmap);

            return getBitmapFromMemCache(path);
    
    public void clearCache() 
        if (mMemoryCache != null) 
            if (mMemoryCache.size() > 0) 
                Log.d("CacheUtils",
                        "mMemoryCache.size() " + mMemoryCache.size());
                mMemoryCache.evictAll();
                Log.d("CacheUtils", "mMemoryCache.size()" + mMemoryCache.size());
            
            mMemoryCache = null;
        
    
    /*
    将图片进行压缩

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true; // 设置了此属性一定要记得将值设置为false
    Bitmap bitmap = null;
    bitmap = BitmapFactory.decodeFile(url, options);
    int be = (int) ((options.outHeight > options.outWidth ? options.outHeight / 150
            : options.outWidth / 200));
    if (be <= 0) // 判断200是否超过原始图片高度
    be = 1; // 如果超过,则不进行缩放
    options.inSampleSize = be;
    options.inPreferredConfig = Bitmap.Config.ARGB_4444;
    options.inPurgeable = true;
    options.inInputShareable = true;
    options.inJustDecodeBounds = false;
    try 
        bitmap = BitmapFactory.decodeFile(url, options);
     catch (OutOfMemoryError e) 
        System.gc();
        Log.e(TAG, "OutOfMemoryError");
    
    */

结语

通过上面的分析,我们了解到LruCache是通过LinkedHashMap来实现,使用LRU算法。

LruCache是对LRU策略的内存缓存的实现,后来的系统源码中也曾经加上该算法的磁盘缓存的实现,也有对应磁盘缓存的源码DiskLruCache.Java。有兴趣的可以自己去看一下。

本篇文章是个人的理解,如有错误请指出。欢迎大家一起交流!

以上是关于Android内存缓存管理LruCache源码解析与示例的主要内容,如果未能解决你的问题,请参考以下文章

Android源码解析——LruCache

Android DiskLruCache完全解析,硬盘缓存的最佳方案

Android 【手撕Glide】--Glide缓存机制(面试)

Android源代码解析之--&gt;LruCache缓存类

Android面试收集录10 LruCache原理解析

硬盘缓存方案DiskLruCache源码解析