说说Android LRU缓存算法实现笔记(二)--LRU的应用

上一篇文章说说Android LRU缓存算法实现学习笔记(一) 中我们介绍了最常用的实现LRU缓存的数据结构LinkedHashMap,这一节我们会针对LinkedHashMap的数据结构的特性,来自己实现缓存结构和学习Android源码和项目中对缓存的完善。

上一篇说到对于缓存实现,我们很重要的会考虑以下几点:1.访问速度;2.逐出旧的缓存策略;3.最好还能考虑到一定的并发度。LinkedHashMap对哈希表的实现保证了我们缓存的快速访问速度,我们通过源码知道,LinkedHashMap默认缓存无限大,所有的节点永远不过期。实际在手机开发中,内存可是寸土寸金,有时候甚至锱铢必较。因此,我们在Android应用中必须重写逐出旧的缓存策略。我自己的简单实现缓存逐出策略如下:

public class LruCache extends LinkedHashMap {
	private static final long serialVersionUID = 1L;
	 /** 最大数据存储容量 */  
    private static final int  LRU_MAX_CAPACITY  = 1024;  

    /** 存储数据容量  */  
    private int  capacity;  

    /** 
     * 默认构造方法 
     */  
    public LruCache() {  
        super();  
    }  
 
    /*
     * 默认缓存最大值为LRU_MAX_CAPACITY
     */
    public LruCache(int initialCapacity, float loadFactor, boolean isLRU) {  
        super(initialCapacity, loadFactor, isLRU);  
        capacity = LRU_MAX_CAPACITY;  
    }  
 
    public LruCache(int initialCapacity, float loadFactor, boolean isLRU, int lruCapacity) {  
        super(initialCapacity, loadFactor, isLRU);  
        this.capacity = lruCapacity;  
    }  

    /**  
     * 重写removeEldestEntry方法,实现重写默认的缓存逐出策略(默认LinkedHashMap下结点永不过期)
     */  
    @Override  
    protected boolean removeEldestEntry(Map.Entry eldest) {  
        if(size() > capacity) {  
            return true;  
        }  
        return false;  
    }  
}
以上的代码在多线程环境下可能就会出现问题,因为我们的Map对象属于多个线程的共享资源,我们必须实现多线程环境下同步访问。多线程环境可以使用时可以使用 Collections.synchronizedMap()方法实现对我们实习的LruCache线程安全操作。

以上的代码我们还可以有另外一种写法,我们不是通过继承来重写LinkedHashMap,可以通过委托(个人觉得聚合关系比较准确)来实现,同时我们需要自己实现对Map访问的线程安全。

public class LruCache  {
	 /** 最大数据存储容量 */  
    private static final int  LRU_MAX_CAPACITY  = 1024;  

    LinkedHashMap map;
    
    /** 存储数据容量  */  
    private int  capacity;  

    /** 
     * 默认构造方法 
     */  
    public LruCache() {  
        super();  
    }  
 
    /*
     * 默认缓存最大值为LRU_MAX_CAPACITY
     */
    public LruCache(int initialCapacity, float loadFactor, boolean isLRU) {  
    	capacity = LRU_MAX_CAPACITY;  
    	map = new LinkedHashMap(initialCapacity, loadFactor, isLRU){
			@Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
        		if(size() > capacity) {  
                    return true;  
                }  
                return false;  
            }
        };  
    }  
 
    public LruCache(int initialCapacity, float loadFactor, boolean isLRU, int lruCapacity) {  
    	this.capacity = lruCapacity;  
    	map = new LinkedHashMap(initialCapacity, loadFactor, isLRU){
    		@Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
        		if(size() > capacity) {  
                    return true;  
                }  
                return false;  
            }
    	};  
    }  
    
    public synchronized void put(K key, V value) {
        map.put(key, value);
    }

    public synchronized V get(K key) {
        return map.get(key);
    }

    public synchronized void remove(K key) {
        map.remove(key);
    }

    public synchronized Set> getAll() {
        return map.entrySet();
    }

    public synchronized int size() {
        return map.size();
    }

    public synchronized void clear() {
        map.clear();
    }
}

以上对LinkedHashMap的缓存结构的线程安全的实现,我们会想,我们能不能提高我们缓存的并发度呢?根据我们已有经验,我们会想到读写锁来实现对读写锁定级别的不同约束来达到多线程下同时多读,独占写来提高缓存并发度。我们可以写如下代码:

public class LruCache  {
	 /** 最大数据存储容量 */  
    private static final int  LRU_MAX_CAPACITY  = 1024;  

    LinkedHashMap map;
    
    private final ReadWriteLock rwlock = new ReentrantReadWriteLock();  
    private final Lock readLock = rwlock.readLock();  
    private final Lock writeLock = rwlock.writeLock(); 
    
    /** 存储数据容量  */  
    private int  capacity;  

    /** 
     * 默认构造方法 
     */  
    public LruCache() {  
        super();  
    }  
 
    /*
     * 默认缓存最大值为LRU_MAX_CAPACITY
     */
    public LruCache(int initialCapacity, float loadFactor, boolean isLRU) {  
    	capacity = LRU_MAX_CAPACITY;  
    	map = new LinkedHashMap(initialCapacity, loadFactor, isLRU){
			@Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
        		if(size() > capacity) {  
                    return true;  
                }  
                return false;  
            }
        };  
    }  
 
    public LruCache(int initialCapacity, float loadFactor, boolean isLRU, int lruCapacity) {  
    	this.capacity = lruCapacity;  
    	map = new LinkedHashMap(initialCapacity, loadFactor, isLRU){
    		@Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
        		if(size() > capacity) {  
                    return true;  
                }  
                return false;  
            }
    	};  
    }  
    
    public void put(K key, V value) {
    	try{
    		writeLock.lock();
    		map.put(key, value);
    	}
    	finally{
    		writeLock.unlock();
    	}
    }

    public synchronized V get(K key) {
    	try{
    		readLock.lock();
    		return map.get(key);
    	}
    	finally{
    		readLock.unlock();
    	}
    }

    public  void remove(K key) {
    	try{
    		readLock.lock();
    		map.remove(key);
    	}
    	finally{
    		readLock.unlock();
    	}
    }

    public  Set> getAll() {
    	try{
    		readLock.lock();
    		return map.entrySet();
    	}
    	finally{
    		readLock.unlock();
    	}
    }

    public  int size() {
    	try{
    		readLock.lock();
    		return map.size();
    	}
    	finally{
    		readLock.unlock();
    	}
    }
    
    public  void clear() {
    	try{
    		readLock.lock();
    		map.clear();
    	}
    	finally{
    		readLock.unlock();
    	}
    }
}
以上的代码在多线程环境下,会发生get和put方法读写锁的争用的问题。我们假设我们的LruCache在多线程环境下访问,当我们多个线程同时执行get方法(读锁),我们知道get方法

public V get(Object key) {
        Entry e = (Entry)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this); //If the enclosing Map is access-ordered, it moves the entry
          to the end of the list; otherwise, it does nothing.
        return e.value;
    }
当我们的LinkedHashMap是按访问顺序排序的,我们把当前结点移动到LinkedHashMap链表结构 header结点的befroe引用指向。因此,我们在多线程同时执行get方法的时候,我们不能保证每次get方法调用的时候,我们每次都能完整的执行recordAccess方法,因此我们的链表结构可能会被破坏。我们看recordAccess方法
void recordAccess(HashMap m) {
            LinkedHashMap lm = (LinkedHashMap)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove(); 
                addBefore(lm.header);
            }
        }
因此我们知道,我们的get方法并不是单纯的读操作,还改变了LinkedHashMap的数据结构,而我们又没有保证多读的情况下get方法对LinkedHashMap的修改为独占的操作,所以,我们不同用读写锁来提高LinkedHashMap的并发度。

读写锁不能提高对Map的并发度,我们会想到在JDK1.5的java.util.concurrent包下的ConcurrentHashMap 对并发的巧妙设计(不熟悉的可以看看我的另一篇文章 Java多线程学习笔记—从Map开始说说同步和并发),我们能不能借鉴ConcurrentHashMap对并发的设计来提高我们Map的并发度。我们知道我们的LinkedHashMap实际上实现的时候继承的HashMap,同时我们也给HashMap的节点增加了两个字段before和after节点。

 private static class Entry extends HashMap.Entry {
        // These fields comprise the doubly linked list used for iteration.
        Entry before, after;
同样我们可以通过继承ConcurrentHashMap来实现高并发的缓存实现。由于自己水平有限,没有能完全体会ConcurrentHashMap精华,整不出来。看到网上有人借鉴ConcurrentHashMap的设计实现的高并发的LRU缓存(参见 ConcurrentHaspLRUHashMap实现初探)。

我们回到Google的对Android缓存的设计,我们首先来看Google推荐的内存缓存LruCache的设计。同样,我们对缓存这几个方面考虑:1.访问速度;2.逐出旧的缓存策略;3.最好还能考虑到一定的并发度。访问速度主要由数据结构决定,LruCache通过委托LinkedHashMap能保证对结点的访问速度。下面我们来看LruCache逐出旧的缓存策略和并发度,我们看LruCache的源码知道,LruCache对LinkedHashMap的get和put方法并不是简单的调用实现,自己重新实现了put和get方法实习。我们先看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); //默认LinkedHashMap的Entry大小不是所占字节大小,默认计数表示大小。如果我们需要精确限定内存大小来逐出旧的eldest节点,我们需要重写safeSizeOf的方法中的SizeOf方法
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value); //节点被移除时候的操作,默认什么操作都不做 ,我们可以在子类重写该方法,比如Android2.3.3(API 10)及之前的版本中,Bitmap对象与其像素数据是分开存储,Bitmap对象存储在heap中,而Bitmap对象的像素数据则存储在Native Memory(本地内存)因此,当Bitmap从缓存逐出的时候,我们还需要手动释放掉Bitamp。这个时候,我们重写entryRemoved方法作用就显现出来啦
        }
        trimToSize(maxSize); //调用该方法通过判断缓存时候达到最大值,尝试去逐出缓存
        return previous;
    }
我们看LruCache的代码实现,我们发现我们在实现put方法的时候,我们找不到LinkedHashMap中的缓存策略的判断方法removeEldestEntry,我们的put实现通过trimToSize方法来实现逐出缓存的策略,因此,我们可以认为我们的LruCache在缓存策略上不再考虑各种情况的缓存策略实现。我们的代码缓存策略不再是模板模式的子类重写父类方法来重写,我们的缓存逐出策略就是trimToSize方法重写sizeOf来定义策略。我们看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()) { //当我们的缓存大小小于maxSize时候,我们不执行逐出缓存
                    break;
                }
                Map.Entry toEvict = map.entrySet().iterator().next(); //我们逐出缓存的策略当我们的缓存大小超过maxSize的时候,我们通过迭代器开始从最旧开始迭代,从map删除节点
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
            entryRemoved(true, key, value, null);
        }
    }
下面再看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.
         */
        V createdValue = create(key); //create方法默认返回null,所以当get没有获得value值的时候默认返回null
        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 += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }
从LruCache的源码实现我们看到,LruCache的实现并没有什么让人惊艳的地方,个人认为重写LinkedHashMap方法的目的为了在Map释放节点entry后,entryRemoved方法能做一些清理的工作。

在我们的实际开发中,我们常结合LruCache和Set>来实现缓存。我们知道从Android2.3开始,用 SoftReference 或者 WeakReference做图片缓存的方法已经不被推荐了。因为DVM 的GC对SoftReference和WeakReference的回收更加频繁,因此我们在使用缓存的时候不能再依赖SoftReference的集合来实现缓存,但是SoftReference仍然可以作为辅助缓存。下面我们以在GitHub上的一个开源实现Android-BitmapCache来学习LruCache强引用和Set>实现内存缓存。

final class BitmapMemoryLruCache extends LruCache {

    private final Set> mRemovedEntries; //此处SoftReference的set集合保存对LruCache的节点Entry执行entryRemoved操作的节点

    private final BitmapLruCache.RecyclePolicy mRecyclePolicy;//该处判断当前Bitmap时候执行手动回收策略

    BitmapMemoryLruCache(int maxSize, BitmapLruCache.RecyclePolicy policy) {
        super(maxSize);

        mRecyclePolicy = policy;
        mRemovedEntries = policy.canInBitmap()
                ? Collections.synchronizedSet(new HashSet>())
                : null;
    }

    CacheableBitmapDrawable put(CacheableBitmapDrawable value) {
        if (null != value) {
            value.setCached(true);
            return put(value.getUrl(), value);
        }

        return null;
    }

    BitmapLruCache.RecyclePolicy getRecyclePolicy() {
        return mRecyclePolicy;
    }

    @Override
    protected int sizeOf(String key, CacheableBitmapDrawable value) { //重写该方法,我们获取Bitmap的精确大小,我们的逐出策略会对大小更敏感,默认逐出策略是根据节点数目的大小来逐出
        return value.getMemorySize();
    }

    @Override
    protected void entryRemoved(boolean evicted, String key, CacheableBitmapDrawable oldValue,//当节点被逐出的时候,放进我们软引用的集合里
            CacheableBitmapDrawable newValue) {
        // Notify the wrapper that it's no longer being cached
        oldValue.setCached(false);

        if (mRemovedEntries != null && oldValue.isBitmapValid() && oldValue.isBitmapMutable()) {
            synchronized (mRemovedEntries) {
                mRemovedEntries.add(new SoftReference(oldValue));
            }
        }
    }

    Bitmap getBitmapFromRemoved(final int width, final int height) { //获取被LruCache逐出的软引用集合的节点Value
        if (mRemovedEntries == null) {
            return null;
        }

        Bitmap result = null;

        synchronized (mRemovedEntries) {
            final Iterator> it = mRemovedEntries.iterator();

            while (it.hasNext()) {
                CacheableBitmapDrawable value = it.next().get();

                if (value != null && value.isBitmapValid() && value.isBitmapMutable()) {
                    if (value.getIntrinsicWidth() == width
                            && value.getIntrinsicHeight() == height) {
                        it.remove();
                        result = value.getBitmap();
                        break;
                    }
                } else {
                    it.remove();
                }
            }
        }

        return result;
    }

    void trimMemory() {
        final Set> values = snapshot().entrySet();

        for (Entry entry : values) {
            CacheableBitmapDrawable value = entry.getValue();
            if (null == value || !value.isBeingDisplayed()) {
                remove(entry.getKey());
            }
        }
    }

}
有对完整代码感兴趣的园友,自己看全部的源码实现Android-BitmapCache(https://github.com/chrisbanes/Android-BitmapCache)。

转载请注明出处:http://blog.csdn.net/johnnyz1234/article/details/43958147





      



你可能感兴趣的:(Android学习记录)