面试题算法程序员

带你了解Android常见的内存缓存算法

2016-07-20  本文已影响263人  程序员徐公

带你了解Android常见的内存缓存算法

本片博客主要简介以下两个问题

  1. 介绍一下常见的内存缓存算法
  2. 怎样实现这些算法

文章首发地址CSDN博客:
大家应该对ImageLoader这个框架都不陌生吧,一个很强大的图片加载框架,虽然作者去年的时候已经停止维护了,但里面的许多东西还是值得我们去学习的。本篇博客讲解的内存缓存算法正是基于ImageLoader的实现基础之上的

常见的几种缓存算法

也就是当内存缓存达到设定的最大值时将内存缓存中近期最少使用的对象移除,有效的避免了OOM的出现。

对每个缓存对象计算他们被使用的频率。把最不常用的缓存对象换走。

这是一个低负载的算法,并且对缓存对象的管理要求不高。通过一个队列去跟踪所有的缓存对象,最近最常用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被踢走,然后把新的缓存对象加进去。

通过绝对的时间周期去失效那些缓存对象。对于新增的对象,保存特定的时间。

超过指定缓存的话,每次移除栈最大内存的缓存的对象

下面我们一起来看一下ImageLoader是怎样实现这些算法的

首先我们一起先来看一下类UML图

源码分析

public abstract class BaseMemoryCache implements MemoryCache {

    /** Stores not strong references to objects */
    private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());

    @Override
    public Bitmap get(String key) {
        Bitmap result = null;
        Reference<Bitmap> reference = softMap.get(key);
        if (reference != null) {
            result = reference.get();
        }
        return result;
    }

    @Override
    public boolean put(String key, Bitmap value) {
        softMap.put(key, createReference(value));
        return true;
    }

    @Override
    public Bitmap remove(String key) {
        Reference<Bitmap> bmpRef = softMap.remove(key);
        return bmpRef == null ? null : bmpRef.get();
    }

    @Override
    public Collection<String> keys() {
        synchronized (softMap) {
            return new HashSet<String>(softMap.keySet());
        }
    }

    @Override
    public void clear() {
        softMap.clear();
    }

    /** Creates {@linkplain Reference not strong} reference of value */
    protected abstract Reference<Bitmap> createReference(Bitmap value);
}

其实就是保存着一份弱引用而已,而它的父类Memory只是定义了几个接口方法,统一标准而已

public abstract class LimitedMemoryCache extends BaseMemoryCache {

    private static final int MAX_NORMAL_CACHE_SIZE_IN_MB = 16;
    private static final int MAX_NORMAL_CACHE_SIZE = MAX_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024;

    private final int sizeLimit;

    private final AtomicInteger cacheSize;

    /**
     * Contains strong references to stored objects. Each next object is added last. If hard cache size will exceed
     * limit then first object is deleted (but it continue exist at {@link #softMap} and can be collected by GC at any
     * time)
     */
    private final List<Bitmap> hardCache = Collections.synchronizedList(
                                                         new LinkedList<Bitmap>());

    /** @param sizeLimit Maximum size for cache (in bytes) */
    public LimitedMemoryCache(int sizeLimit) {
        this.sizeLimit = sizeLimit;
        cacheSize = new AtomicInteger();
        if (sizeLimit > MAX_NORMAL_CACHE_SIZE) {
            L.w("You set too large memory cache size (more than %1$d Mb)", 
                                                         MAX_NORMAL_CACHE_SIZE_IN_MB);
        }
    }

    @Override
    public boolean put(String key, Bitmap value) {
        boolean putSuccessfully = false;
        // Try to add value to hard cache
        int valueSize = getSize(value);
        int sizeLimit = getSizeLimit();
        int curCacheSize = cacheSize.get();
        if (valueSize < sizeLimit) {
            while (curCacheSize + valueSize > sizeLimit) {
                Bitmap removedValue = removeNext();
                if (hardCache.remove(removedValue)) {
                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                }
            }
            hardCache.add(value);
            cacheSize.addAndGet(valueSize);

            putSuccessfully = true;
        }
        // Add value to soft cache
        super.put(key, value);
        return putSuccessfully;
    }

    @Override
    public Bitmap remove(String key) {
        Bitmap value = super.get(key);
        if (value != null) {
            if (hardCache.remove(value)) {
                cacheSize.addAndGet(-getSize(value));
            }
        }
        return super.remove(key);
    }

    @Override
    public void clear() {
        hardCache.clear();
        cacheSize.set(0);
        super.clear();
    }

    protected int getSizeLimit() {
        return sizeLimit;
    }

    protected abstract int getSize(Bitmap value);

    protected abstract Bitmap removeNext();
}

LimitedMemoryCache所做的工作可以分为以下几步

   private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());

注意事项

结合BaseMemoryCache和LimitedMemoryCache,我们可以知道LimitedMemoryCache的子类,至少可以访问两份Bitmap 的缓存,一份是BaseMemoryCache所拥有的softMap ,是弱引用;一份是LimitedMemoryCachehar所拥有的hardCache 集合

//父类BaseMemoryCache的成员变量,并且每次在操作的时候都会把bitmap的弱引用存进去
 private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(
                new HashMap<String, Reference<Bitmap>>());
                
//LimitedMemoryCache的成员变量,缓存的bitmap是强引用
private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());                

有人可能会有疑问了这些成员变量不是私有的吗?为什么说LimitedMemoryCache的子类,至少可以访问两份引用,这点我们可以从他们的put方法中知道

@Override
public boolean put(String key, Bitmap value) {
   boolean putSuccessfully = false;
   // Try to add value to hard cache
   int valueSize = getSize(value);
   int sizeLimit = getSizeLimit();
   int curCacheSize = cacheSize.get();
   if (valueSize < sizeLimit) {
      while (curCacheSize + valueSize > sizeLimit) {
         Bitmap removedValue = removeNext();
         if (hardCache.remove(removedValue)) {
            curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
         }
      }
      hardCache.add(value);
      cacheSize.addAndGet(valueSize);

      putSuccessfully = true;
   }
   // Add value to soft cache
   super.put(key, value);
   return putSuccessfully;
}

同理LimitedMemoryCache的子类put也会调用LimitedMemoryCache的put方法,代码见下面分析。

同时从上面的分析当中我们可以知道主要关心put和removeNext()这两个方法就可以了,put()方法其实就是把bitmap对象存进我们的queue队列中

下面我们在看一下UsingFreqLimitedMemoryCache是怎样实现的?

public class UsingFreqLimitedMemoryCache extends LimitedMemoryCache {
    /**
     * Contains strong references to stored objects (keys) and last object usage date (in milliseconds). If hard cache
     * size will exceed limit then object with the least frequently usage is deleted (but it continue exist at
     * {@link #softMap} and can be collected by GC at any time)
     */
    private final Map<Bitmap, Integer> usingCounts = Collections.synchronizedMap(new HashMap<Bitmap, Integer>());

    public UsingFreqLimitedMemoryCache(int sizeLimit) {
        super(sizeLimit);
    }

    @Override
    public boolean put(String key, Bitmap value) {
        if (super.put(key, value)) {
            usingCounts.put(value, 0);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Bitmap get(String key) {
        Bitmap value = super.get(key);
        // Increment usage count for value if value is contained in hardCahe
        if (value != null) {
            Integer usageCount = usingCounts.get(value);
            if (usageCount != null) {
                usingCounts.put(value, usageCount + 1);
            }
        }
        return value;
    }

    @Override
    public Bitmap remove(String key) {
        Bitmap value = super.get(key);
        if (value != null) {
            usingCounts.remove(value);
        }
        return super.remove(key);
    }

    @Override
    public void clear() {
        usingCounts.clear();
        super.clear();
    }

    @Override
    protected int getSize(Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

    @Override
    protected Bitmap removeNext() {
        Integer minUsageCount = null;
        Bitmap leastUsedValue = null;
        Set<Entry<Bitmap, Integer>> entries = usingCounts.entrySet();
        synchronized (usingCounts) {
            for (Entry<Bitmap, Integer> entry : entries) {
                if (leastUsedValue == null) {
                    leastUsedValue = entry.getKey();
                    minUsageCount = entry.getValue();
                } else {
                    Integer lastValueUsage = entry.getValue();
                    if (lastValueUsage < minUsageCount) {
                        minUsageCount = lastValueUsage;
                        leastUsedValue = entry.getKey();
                    }
                }
            }
        }
        usingCounts.remove(leastUsedValue);
        return leastUsedValue;
    }

    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

思路解析

  1. 当我们调用put方法,把bitmap存进内存的时候,他会判断是否超出我们的最大值,超出我们的最大值就会调用removeNext();来获得我们将要移除的bitmap对象,最终再调用hardCache.remove(removedValue)去移除它。

@Override
public boolean put(String key, Bitmap value) {
  boolean putSuccessfully = false;
  // Try to add value to hard cache
  int valueSize = getSize(value);
  int sizeLimit = getSizeLimit();
  int curCacheSize = cacheSize.get();
  if (valueSize < sizeLimit) {
     while (curCacheSize + valueSize > sizeLimit) {
        Bitmap removedValue = removeNext();
        if (hardCache.remove(removedValue)) {
           curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
        }
     }
     hardCache.add(value);
     cacheSize.addAndGet(valueSize);

     putSuccessfully = true;
  }
  // Add value to soft cache
  super.put(key, value);
  return putSuccessfully;
}
···

* 下面我们来看一下removeNext()是怎样获得将要移除的bitmap对象的?

```java
private final Map<Bitmap, Integer> usingCounts = Collections.
                            synchronizedMap(new HashMap<Bitmap, Integer>());


@Override
protected Bitmap removeNext() {
  Integer minUsageCount = null;
  Bitmap leastUsedValue = null;
  Set<Entry<Bitmap, Integer>> entries = usingCounts.entrySet();
  synchronized (usingCounts) {
     for (Entry<Bitmap, Integer> entry : entries) {
        if (leastUsedValue == null) {
           leastUsedValue = entry.getKey();
           minUsageCount = entry.getValue();
        } else {
           Integer lastValueUsage = entry.getValue();
           if (lastValueUsage < minUsageCount) {
              minUsageCount = lastValueUsage;
              leastUsedValue = entry.getKey();
           }
        }
     }
  }
  usingCounts.remove(leastUsedValue);
  return leastUsedValue;
}


其实就是将usingCounts中出现次数最少的节点移除掉。

那它是在什么时候计算bitmap的使用次数的呢?相信大多数人会想到,既然是使用频率,那肯定是在取图片的过程中计算的,没错,下面让我们一起来看一下是怎样实现的? 其实也很简单,判断是否存在缓存value,存在的话,使用次数加一

@Override
public Bitmap get(String key) {
  Bitmap value = super.get(key);
  // Increment usage count for value if value is contained in hardCahe
  if (value != null) {
     Integer usageCount = usingCounts.get(value);
     if (usageCount != null) {
        usingCounts.put(value, usageCount + 1);
     }
  }
  return value;
}


好的,到此UsingFreqLimitedMemoryCache的源码分析位置


FIFOLimitedMemoryCache源码分析

public class FIFOLimitedMemoryCache extends LimitedMemoryCache {

    private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());

    public FIFOLimitedMemoryCache(int sizeLimit) {
        super(sizeLimit);
    }

    @Override
    public boolean put(String key, Bitmap value) {
        if (super.put(key, value)) {
            queue.add(value);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Bitmap remove(String key) {
        Bitmap value = super.get(key);
        if (value != null) {
            queue.remove(value);
        }
        return super.remove(key);
    }

    @Override
    public void clear() {
        queue.clear();
        super.clear();
    }

    @Override
    protected int getSize(Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

    @Override
    protected Bitmap removeNext() {
        return queue.remove(0);
    }

    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}


@Override
public Bitmap get(String key) {
  Bitmap result = null;
  Reference<Bitmap> reference = softMap.get(key);
  if (reference != null) {
     result = reference.get();
  }
  return result;
}

LargestLimitedMemoryCache源码分析

public class LargestLimitedMemoryCache extends LimitedMemoryCache {
    /**
     * Contains strong references to stored objects (keys) and sizes of the objects. If hard cache
     * size will exceed limit then object with the largest size is deleted (but it continue exist at
     * {@link #softMap} and can be collected by GC at any time)
     */
    private final Map<Bitmap, Integer> valueSizes = Collections.synchronizedMap(new HashMap<Bitmap, Integer>());

    public LargestLimitedMemoryCache(int sizeLimit) {
        super(sizeLimit);
    }

    @Override
    public boolean put(String key, Bitmap value) {
        if (super.put(key, value)) {
            valueSizes.put(value, getSize(value));
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Bitmap remove(String key) {
        Bitmap value = super.get(key);
        if (value != null) {
            valueSizes.remove(value);
        }
        return super.remove(key);
    }

    
 //这里我们省略若干个方法,有兴趣的话讲源码去,下面有提供源码下载地址
    @Override
    protected Bitmap removeNext() {
        Integer maxSize = null;
        Bitmap largestValue = null;
        Set<Entry<Bitmap, Integer>> entries = valueSizes.entrySet();
        synchronized (valueSizes) {
            for (Entry<Bitmap, Integer> entry : entries) {
                if (largestValue == null) {
                    largestValue = entry.getKey();
                    maxSize = entry.getValue();
                } else {
                    Integer size = entry.getValue();
                    if (size > maxSize) {
                        maxSize = size;
                        largestValue = entry.getKey();
                    }
                }
            }
        }
        valueSizes.remove(largestValue);
        return largestValue;
    }

}

同样我们只关心put方法和removeNext()方法

@Override
public boolean put(String key, Bitmap value) {
    if (super.put(key, value)) {
         valueSizes.put(value, getSize(value));
         return true;
    }else {
         return false;
    }
}

@Override
protected Bitmap removeNext() {
   Integer maxSize = null;
   Bitmap largestValue = null;
   Set<Entry<Bitmap, Integer>> entries = valueSizes.entrySet();
   synchronized (valueSizes) {
      for (Entry<Bitmap, Integer> entry : entries) {
         if (largestValue == null) {
            largestValue = entry.getKey();
            maxSize = entry.getValue();
         } else {
            Integer size = entry.getValue();
            if (size > maxSize) {
               maxSize = size;
               largestValue = entry.getKey();
            }
         }
      }
   }
   valueSizes.remove(largestValue);
   return largestValue;
}

   

下面我们来看一下LruMemoryCache是怎样实现的

源码我们就不贴出来了,主要逻辑在put方法中

// 存储bitmap对象,在构造方法里面初始化
private final LinkedHashMap<String, Bitmap> map;


/** Caches {@code Bitmap} for {@code key}. The Bitmap is moved to the head of the queue. */
@Override
public final boolean put(String key, Bitmap value) {
   if (key == null || value == null) {
      throw new NullPointerException("key == null || value == null");
   }

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

   trimToSize(maxSize);
   return true;
}

当我们把bitmap存进内存的时候,他会trimToSize(maxSize)这个方法去判断我们是否超过我们规定内存的最大值,超过的话移除掉最先添加进来的那个

private void trimToSize(int maxSize) {
   while (true) {
      String key;
      Bitmap 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<String, Bitmap> toEvict = map.entrySet().iterator().next();
         if (toEvict == null) {
            break;
         }
         key = toEvict.getKey();
         value = toEvict.getValue();
         map.remove(key);
         size -= sizeOf(key, value);
      }
   }
}


下面我们来看一下LimitedAgeMemoryCache 是怎样实现的

/**
 * Decorator for {@link MemoryCache}. Provides special feature for cache: if some cached object age exceeds defined
 * value then this object will be removed from cache.
 *
 * 采用装饰着模式,计算对象的最大存活时间
 * 在get方法的时候判断大于的移除掉
 *
 */
public class LimitedAgeMemoryCache implements MemoryCache {

    private final MemoryCache cache;

    private final long maxAge;
    private final Map<String, Long> loadingDates = Collections.synchronizedMap(new HashMap<String, Long>());

    /**
     * @param cache  Wrapped memory cache
     * @param maxAge Max object age <b>(in seconds)</b>. If object age will exceed this value then it'll be removed from
     *               cache on next treatment (and therefore be reloaded).
     */
    public LimitedAgeMemoryCache(MemoryCache cache, long maxAge) {
        this.cache = cache;
        this.maxAge = maxAge * 1000; // to milliseconds
    }

    @Override
    public boolean put(String key, Bitmap value) {
        boolean putSuccesfully = cache.put(key, value);
        if (putSuccesfully) {
            loadingDates.put(key, System.currentTimeMillis());
        }
        return putSuccesfully;
    }

    @Override
    public Bitmap get(String key) {
        Long loadingDate = loadingDates.get(key);
        if (loadingDate != null && System.currentTimeMillis() - loadingDate > maxAge) {
            cache.remove(key);
            loadingDates.remove(key);
        }

        return cache.get(key);
    }

    @Override
    public Bitmap remove(String key) {
        loadingDates.remove(key);
        return cache.remove(key);
    }

    @Override
    public Collection<String> keys() {
        return cache.keys();
    }

    @Override
    public void clear() {
        cache.clear();
        loadingDates.clear();
    }
}

分析


//成员变量,保持存活时间的map集合
private final Map<String, Long> loadingDates = Collections.synchronizedMap(
                                                      new HashMap<String, Long>());

@Override
public boolean put(String key, Bitmap value) {
   boolean putSuccesfully = cache.put(key, value);
   if (putSuccesfully) {
      loadingDates.put(key, System.currentTimeMillis());
   }
   return putSuccesfully;
}

感觉ImageLoader在实现FIFOLimitedMemoryCache算法的时候还是有一点缺陷,为什么呢?

到此ImageLoader内存的缓存算法源码分析为止,下面我稍微改一下实现方式,内存里面不再保存着两份引用了,bitmap的缓存只保存着一份引用。


自己实现的内存缓存算法

类UML图

其实跟ImageLoader实现的也没有多大区别,只是我去除了弱引用,每个实现类里面不像LimitedMemoryCache的实现类一样持有两份引用而已

首先我们来看一下LimitedMemoryCache是怎样实现的

public abstract class LimitedMemoryCache implements MemoryCache {

    private static final int MAX_NORMAL_CACHE_SIZE_IN_MB = 16;
    private static final int MAX_NORMAL_CACHE_SIZE = MAX_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024;

    private final int sizeLimit;
     public static final String TAG="tag";

    private final AtomicInteger cacheSize;

    private final Map<String, Bitmap> mMap= Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>());



    public LimitedMemoryCache(int sizeLimit) {
        this.sizeLimit = sizeLimit;

        cacheSize = new AtomicInteger();
        if (sizeLimit > MAX_NORMAL_CACHE_SIZE) {
            Log.w(TAG,"You set too large memory cache size (more than %1$d Mb)"+ MAX_NORMAL_CACHE_SIZE_IN_MB);
        }

    }

    @Override
    public boolean put(String key, Bitmap value) {
        boolean putSuccessfully = false;
        // Try to add value to hard cache
        int valueSize = getSize(value);
        int sizeLimit = getSizeLimit();
        int curCacheSize = cacheSize.get();
        if (valueSize < sizeLimit) {
            while (curCacheSize + valueSize > sizeLimit) {
                String removeKey = removeNext();
                if(removeKey==null){
                   break;
                }
                Bitmap bitmap = mMap.remove(key);
                if(bitmap!=null){
                    curCacheSize = cacheSize.addAndGet(-getSize(bitmap));
                }
            }
            mMap.put(key,value);
            cacheSize.addAndGet(valueSize);
            putSuccessfully = true;
        }

        return putSuccessfully;
    }

    @Override
    public Bitmap remove(String key) {
        return  mMap.remove(key);
    }

    @Override
    public Bitmap get(String key) {
        return mMap.get(key);
    }

    @Override
    public void clear() {
        mMap.clear();
        cacheSize.set(0);
    }

    protected int getSizeLimit() {
        return sizeLimit;
    }

    @Override
    public Collection<String> keys() {
        synchronized (mMap) {
            return new HashSet<String>(mMap.keySet());
        }
    }

    protected abstract int getSize(Bitmap value);

    protected abstract String removeNext();
}

LimitedMemoryCache所做的工作可以分为以下几步

//在构造方法里面初始化      
private final Map<String, Bitmap> mMap;

下面我们来看一下

public class UsingFreqLimitedMemoryCache extends LimitedMemoryCache {

    private final Map<String, Integer> usingCounts = Collections.synchronizedMap(new HashMap<String, Integer>());

//这里省略了若干个方法
    @Override
    public boolean put(String key, Bitmap value) {
        if (super.put(key, value)) {
            usingCounts.put(key, 0);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Bitmap get(String key) {
        Bitmap value = super.get(key);
        // Increment usage count for value if value is contained in hardCahe
        if (value != null) {
            Integer usageCount = usingCounts.get(value);
            if (usageCount != null) {
                usingCounts.put(key, usageCount + 1);
            }
        }
        return value;
    }

    @Override
    public Bitmap remove(String key) {
        usingCounts.remove(key);
        return super.remove(key);
    }


    @Override
    protected String removeNext() {
        Integer minUsageCount = null;
        String leastUsedValue = null;
        Set<Entry<String, Integer>> entries = usingCounts.entrySet();
        synchronized (usingCounts) {
            for (Entry<String, Integer> entry : entries) {
                if (leastUsedValue == null) {
                    leastUsedValue = entry.getKey();
                    minUsageCount = entry.getValue();
                } else {
                    Integer lastValueUsage = entry.getValue();
                    if (lastValueUsage < minUsageCount) {
                        minUsageCount = lastValueUsage;
                        leastUsedValue = entry.getKey();
                    }
                }
            }
        }
        usingCounts.remove(leastUsedValue);
        return leastUsedValue;
    }

}

与ImageLoader不同的是,我们是用这个记录

private final Map<String, Integer> usingCounts = Collections.synchronizedMap(new HashMap<String, Integer>());

而ImageLoader是用这种类型的记录,其他的基本大同小异,有兴趣的可以去这里下载源码

private final Map<Bitmap, Integer> usingCounts = Collections.synchronizedMap(new HashMap<Bitmap, Integer>());

题外话:通过这篇博客,学习了更多的缓存算法,同时你们有没有发现,很多地方都用到了Collection框架,而要用好这些,个人觉得去了解他们的原理是非常必要的,尤其是map和List集合,不管说是初学者还是大牛,毕竟万丈高楼也是从平地盖起的,基础非常重要

**转载请注明原博客地址: **

源码参考地址 ImagerLoader:

**源码下载地址: **

上一篇下一篇

猜你喜欢

热点阅读