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

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

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

public class LruCache<K,V> extends LinkedHashMap<K, V> {
	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<K,V> eldest) {
        if(size() > capacity) {
            return true;
        }
        return false;
    }
}

以上的代码在多线程环境下可能就会出现问题,因为我们的Map对象属于多个线程的共享资源,我们必须实现多线程环境下同步访问。多线程环境可以使用时可以使用 Collections.synchronizedMap()方法实现对我们实习的LruCache线程安全操作。

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

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

    LinkedHashMap<K, V> 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<K,V>(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<K,V>(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<Map.Entry<K, V>> getAll() {
        return map.entrySet();
    }

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

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

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

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

    LinkedHashMap<K, V> 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<K,V>(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<K,V>(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<Map.Entry<K, V>> 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<K,V> e = (Entry<K,V>)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<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)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<K,V> extends HashMap.Entry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> 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<K, V> 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<SoftReference<Bitmap>>来实现缓存。我们知道从Android2.3开始,用 SoftReference 或者 WeakReference做图片缓存的方法已经不被推荐了。因为DVM 的GC对SoftReference和WeakReference的回收更加频繁,因此我们在使用缓存的时候不能再依赖SoftReference的集合来实现缓存,但是SoftReference仍然可以作为辅助缓存。下面我们以在GitHub上的一个开源实现Android-BitmapCache来学习LruCache强引用和Set<SoftReference<Bitmap>>实现内存缓存。

final class BitmapMemoryLruCache extends LruCache<String, CacheableBitmapDrawable> {

    private final Set<SoftReference<CacheableBitmapDrawable>> 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<SoftReference<CacheableBitmapDrawable>>())
                : 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<CacheableBitmapDrawable>(oldValue));
            }
        }
    }

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

        Bitmap result = null;

        synchronized (mRemovedEntries) {
            final Iterator<SoftReference<CacheableBitmapDrawable>> 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<Entry<String, CacheableBitmapDrawable>> values = snapshot().entrySet();

        for (Entry<String, CacheableBitmapDrawable> 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

时间: 2024-08-25 04:15:25

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

缓存算法(FIFO 、LRU、LFU三种算法的区别)

缓存算法(FIFO .LRU.LFU三种算法的区别) FIFO算法# FIFO 算法是一种比较容易实现的算法.它的思想是先进先出(FIFO,队列),这是最简单.最公平的一种思想,即如果一个数据是最先进入的,那么可以认为在将来它被访问的可能性很小.空间满的时候,最先进入的数据会被最早置换(淘汰)掉. FIFO 算法的描述:设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能: set(key,value):将记录(key,value)插入该结构.当缓存满时,将最先进入缓存的数据置

android图片缓存框架Android-Universal-Image-Loader(二)

这篇打算直接告诉大家怎么用吧,其实这个也不是很难的框架,大致使用过程如下: // 获取缓存图片目录 File cacheDir = StorageUtils.getOwnCacheDirectory(activity, "imageloader/Cache"); ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder( activity).memoryCacheExtraOptions(800, 76

面试挂在了 LRU 缓存算法设计上

好吧,有人可能觉得我标题党了,但我想告诉你们的是,前阵子面试确实挂在了 RLU 缓存算法的设计上了.当时做题的时候,自己想的太多了,感觉设计一个 LRU(Least recently used) 缓存算法,不会这么简单啊,于是理解错了题意(我也是服了,还能理解成这样,,,,),自己一波操作写了好多代码,后来卡住了,再去仔细看题,发现自己应该是理解错了,就是这么简单,设计一个 LRU 缓存算法. 不过这时时间就很紧了,按道理如果你真的对这个算法很熟,十分钟就能写出来了,但是,自己虽然理解 LRU

说说Android LRU缓存算法实现学习笔记(一)

在我们的手机应用开发时候,我们经常会遇到大数据访问的时候,我们通常会考虑以下几个方面的情况.一.手机内存的限制还必须保证应用反应的流畅:二.尽量小的流量消耗,不然,你的应用流畅度再好体验再好,用户还是会毫不犹豫的卸载掉你的应用.大数据量访问的情况下,数据缓存是我们一定会考虑到的解决方案.而作为缓存,我们很重要的会考虑以下几点:1.访问速度:2.逐出旧的缓存策略:3.最好还能考虑到一定的并发度.这篇我们主要说说LRU策略的缓存算法实现,我们就用图片缓存为例来谈谈Android应用开发中的缓存实现.

LRU缓存算法与pylru

这篇写的略为纠结,算法原理.库都是现成的,我就调用了几个函数而已,这有啥好写的?不过想了想,还是可以介绍一下LRU算法的原理及简单的用法. LRU(Least Recently Used,最近最少使用)是一种内存页面置换算法.什么叫内存页面置换?我们知道,相对于内存的速度来讲,磁盘的速度是很慢的.我们需要查询数据的时候,不能每次都跑到磁盘去查,需要在内存里设置一块空间,把一些常用的数据放在这块空间里,以后查的时候就直接在这里查,而不必去磁盘,从而起到“加速”的作用.但是这块空间肯定是远远小于磁盘

LRU缓存算法

引子: 我们平时总会有一个电话本记录所有朋友的电话,但是,如果有朋友经常联系,那些朋友的电话号码不用翻电话本我们也能记住,但是,如果长时间没有联系了,要再次联系那位朋友的时候,我们又不得不求助电话本,但是,通过电话本查找还是很费时间的.但是,我们大脑能够记住的东西是一定的,我们只能记住自己最熟悉的,而长时间不熟悉的自然就忘记了. 其实,计算机也用到了同样的一个概念,我们用缓存来存放以前读取的数据,而不是直接丢掉,这样,再次读取的时候,可以直接在缓存里面取,而不用再重新查找一遍,这样系统的反应能力

LinkedHashMap实现LRU缓存算法

LinkedHashMap的get()方法除了返回元素之外还可以把被访问的元素放到链表的底端,这样一来每次顶端的元素就是remove的元素. 构造函数如下: public LinkedHashMap (int initialCapacity, float loadFactor, boolean accessOrder): initialCapacity   初始容量 loadFactor    加载因子,一般是 0.75f accessOrder   false基于插入顺序,true 基于访问顺

HashMap+双向链表手写LRU缓存算法/页面置换算法

import java.util.Hashtable; //https://zhuanlan.zhihu.com/p/34133067 class DLinkedList { String key; //键 int value; //值 DLinkedList pre; //双向链表前驱 DLinkedList next; //双向链表后继 } public class LRUCache { private Hashtable<String,DLinkedList> cache = new H

Android第一行代码学习笔记二---在活动中使用Toast

Toast:是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间. 首先需要定义一个弹出Toast触发点,接着笔记一的程序,正好上面有个按钮,我们就点击这个按钮的时候弹出来一个Toast,在onCreate()方法中添加如下代码: protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceStat