Android LRUCache原理及源码分析(阿里巴巴)
来源:互联网 发布:新手开农村淘宝网店 编辑:程序博客网 时间:2024/06/05 18:21
1.简介
LRU 是 Least Recently Used 最近最少使用算法。
2.使用方法
当成一个 Map 用就可以了,只不过实现了 LRU 缓存策略。
使用的时候记住几点即可:
- (必要)你需要提供一个缓存容量作为构造参数。
- (必要)覆写 sizeOf 方法 ,自定义设计一条数据放进来的容量计算,如果不覆写就无法预知数据的容量,不能保证缓存容量限定在最大容量以内。
- (非必要)覆写 entryRemoved 方法,你可以知道最少使用的缓存被清除时的数据(evicted, key, oldValue, newVaule)。
- (非必要)覆写 create 方法 。
- LruCache是线程安全的,在内部的 get、put、remove 包括 trimToSize 都是安全的(因为都上锁了)。
Demo
//获取系统分配给每个应用程序的最大内存,每个应用系统分配32M int maxMemory = (int) Runtime.getRuntime().maxMemory(); int mCacheSize = maxMemory / 8; private LruCache<String, Bitmap> mMemoryCache;//给LruCache分配1/8 4M mMemoryCache = new LruCache<String, Bitmap>(mCacheSize){ //必须重写此方法,来测量Bitmap的大小 @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } };
3.源码分析
3.1LruCache 原理概要解析
LruCache 就是 利用 LinkedHashMap 的一个特性(accessOrder=true
基于访问顺序)再加上对 LinkedHashMap 的数据操作上锁实现的缓存策略。
LruCache 的数据缓存是内存中的。
- 首先设置了内部 LinkedHashMap 构造参数
accessOrder=true
, 实现了数据排序按照访问顺序。 - 后在每次
LruCache.get(K key)
方法里都会调用LinkedHashMap.get(Object key)
。 - 如上述设置了
accessOrder=true
后,每次LinkedHashMap.get(Object key)
都会进行LinkedHashMap.makeTail(LinkedEntry<K, V> e)
。 - LinkedHashMap 是双向循环链表,然后每次
LruCache.get -> LinkedHashMap.get
的数据就被放到最末尾了。 - 在
put
和trimToSize
的方法执行下,如果发生数据量移除,会优先移除掉最前面的数据(因为最新访问的数据在尾部)。
3.2 LruCache 的唯一构造方法
/** * LruCache的构造方法:需要传入最大缓存个数 */public LruCache(int maxSize) { ... this.maxSize = maxSize; /* * 初始化LinkedHashMap * 第一个参数:initialCapacity,初始大小 * 第二个参数:loadFactor,负载因子=0.75f * 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序,将LinkedHashMap的accessOrder设置为true来实现LRU */ this.map = new LinkedHashMap<K, V>(0, 0.75f, true);}
第一个参数 initialCapacity 用于初始化该 LinkedHashMap 的大小。
先简单介绍一下 第二个参数 loadFactor,这个其实的 HashMap 里的构造参数,涉及到扩容问题,比如 HashMap 的最大容量是100,那么这里设置0.75f的话,到75容量的时候就会扩容。
主要是第三个参数 accessOrder=true
,这样的话 LinkedHashMap 数据排序就会基于数据的访问顺序,从而实现了 LruCache 核心工作原理。
3.3 LruCache.get(K key)
/* *通过key获取相应的item,或者创建返回相应的item。相应的item会移动到队列的尾部。 *如果item的value没有被cache或者不能被创建,则返回null。 */public final V get(K key) { ... V mapValue; synchronized (this) { // 关键点:LinkedHashMap每次get都会基于访问顺序来重整数据顺序 mapValue = map.get(key); // 计算 命中次数 if (mapValue != null) { hitCount++; return mapValue; } // 计算 丢失次数 missCount++; } /* * 官方解释: * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。 * 如果在create()执行的时候,一个冲突的值被添加到Map,我们在Map中删除这个值,释放被创造的值。 */ V createdValue = create(key); if (createdValue == null) { return null; } /*************************** * 不覆写create方法走不到下面 * ***************************/ /* * 正常情况走不到这里 * 因为默认的 create(K key) 逻辑为null * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑,比如返回了一个不为空的默认值 */ synchronized (this) { // 记录 create 的次数 createCount++; // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值 mapValue = map.put(key, createdValue); // 如果之前存在相同key的value,即有冲突。 if (mapValue != null) { /* * 有冲突 * 所以 撤销 刚才的 操作 * 将 之前相同key 的值 重新放回去 */ map.put(key, mapValue); } else { // 加入新创建的对象之后需要重新计算size大小 size += safeSizeOf(key, createdValue); } } // 如果上面 判断出了 将要放入的值发生冲突 if (mapValue != null) { /* * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了 * 告诉 自定义 的 entryRemoved 方法 */ entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { // 上面进行了size += 操作,每次新加入对象都需要调用trimToSize方法看是否需要回收 trimToSize(maxSize); return createdValue; }}
上述的 get 方法表面并没有看出哪里有实现了 LRU 的缓存策略。主要在 mapValue = map.get(key)
;里,调用了 LinkedHashMap 的 get
方法,再加上 LruCache 构造里默认设置 LinkedHashMap 的 accessOrder=true
。
3.4 LinkedHashMap.get(Object key)
/** * Returns the value to which the specified key is mapped, * or {@code null} if this map contains no mapping for the key. * * <p>More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * * <p>A return value of {@code null} does not <i>necessarily</i> * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. */ public V get(Object key) { LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key); if (e == null) return null; e.recordAccess(this); return e.value; }
3.5 LinkedHashMapEntry.recordAccess(HashMap<K,V> m)
/** * This method is invoked by the superclass whenever the value * of a pre-existing entry is read by Map.get or modified by Map.set. * If the enclosing Map is access-ordered, it moves the entry * to the end of the list; otherwise, it does nothing. */ void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } }
关键代码:如果 if (lm.accessOrder)
那么每次 get
都会执行 remove()
和addBefore(lm.header)
,LinkedHashMap 是双向循环链表,然后此次 LruCache.get -> LinkedHashMap.get 的数据就被放到最末尾了。
3.5 LinkedHashMapEntry.remove()
/** * Removes this entry from the linked list. */private void remove() { before.after = after; after.before = before;}
3.6 LinkedHashMapEntry.addBefore(LinkedHashMapEntry<K,V> existingEntry)
/** * 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;}
接下来介绍 LruCache 的容量溢出策略。
3.7 LruCache.put(K key, V value)
/** * Caches {@code value} for {@code key}. The value is moved to the head of the queue. * * @return the previous value mapped by {@code key}. */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; }
记住几点:
- put 开始的时候确实是把值放入 LinkedHashMap 了,不管超不超过你设定的缓存容量。
- 然后根据
safeSizeOf
方法计算 此次添加数据的容量是多少,并且加到size
里 。 - 说到
safeSizeOf
就要讲到sizeOf(K key, V value)
会计算出此次添加数据的大小 。 - 直到
put
要结束时,进行了trimToSize
才判断size
是否 大于maxSize
然后进行最近很少访问数据的移除。
3.8 LruCache.trimToSize(int maxSize)
/** * Remove the eldest entries until the total of remaining entries is at or below the requested size. * * @param maxSize the maximum size of the cache before returning. May be -1 to evict even 0-sized elements. * 方法根据maxSize来调整内存cache的大小,如果maxSize传入-1,则清空缓存中的所有对象 */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) { break; } Map.Entry<K, V> toEvict = map.eldest(); if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; //回收次数+1 } entryRemoved(true, key, value, null); } }
/** * Returns the eldest entry in the map, or {@code null} if the map is empty. * * Android-added. * * @hide */public Map.Entry<K, V> eldest() { Entry<K, V> eldest = header.after; return eldest != header ? eldest : null;}
简单描述:会判断之前size
是否大于maxSize
。是的话,直接跳出后什么也不做。不是的话,证明已经溢出容量了。由前面论述可知,最近经常访问的数据在表头。拿到一个存放 key 的 Set,然后一直一直从头开始删除,删一个判断是否溢出,直到没有溢出。
3.9 覆写 entryRemoved 的作用
entryRemoved被LruCache调用的场景:
- 1.(put) put 发生 key 冲突时被调用,evicted=false,key=此次 put 的 key,oldValue=被覆盖的冲突 value,newValue=此次 put 的 value。
- 2.(trimToSize) trimToSize 的时候,只会被调用一次,就是最后一次被删除的最少访问数据带回来。evicted=true,key=最后一次被删除的 key,oldValue=最后一次被删除的 value,newValue=null(此次没有冲突,只是 remove)。
- 3.(remove) remove的时候,存在对应 key,并且被成功删除后被调用。evicted=false,key=此次 put的 key,oldValue=此次删除的 value,newValue=null(此次没有冲突,只是 remove)。
- 4.(get后半段,查询丢失后处理情景,不过建议忽略) get 的时候,正常的话不实现自定义 create 的话,代码上看 get 方法只会走一半,如果你实现了自定义的 create(K key) 方法,并且在 你 create 后的值放入 LruCache 中发生 key 冲突时被调用,evicted=false,key=此次 get 的 key,oldValue=被你自定义 create(key)后的 value,newValue=原本存在 map 里的 key-value。
解释一下第四点吧:<1>.第四点是这样的,先 get(key),然后没拿到,丢失。<2>.如果你提供了 自定义的 create(key) 方法,那么 LruCache 会根据你的逻辑自造一个 value,但是当放入的时候发现冲突了,但是已经放入了。<3>.此时,会将那个冲突的值再让回去覆盖,此时调用上述 4.的 entryRemoved。
因为 HashMap 在数据量大情况下,拿数据可能造成丢失,导致前半段查不到,你自定义的 create(key) 放入的时候发现又查到了(有冲突)。然后又急忙把原来的值放回去,此时你就白白create一趟,无所作为,还要走一遍entryRemoved。
综上就如同注释写的一样:
/** * 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用 * 或者替换条目值时put调用,默认实现什么都没做。 * 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。 * 3.evicted=true:如果该条目被删除空间(表示进行了trimToSize or remove)evicted=false:put冲突后 或 get里成功create后导致 * 4.newValue!=null,那么则被put()或get()调用。 */protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
4. 总结
- LruCache 是通过 LinkedHashMap 构造方法的第三个参数的
accessOrder=true
实现了 LinkedHashMap 的数据排序基于访问顺序 (最近访问的数据会在链表尾部),在容量溢出的时候,将链表头部的数据移除。从而,实现了 LRU 数据缓存机制。 - LruCache 在内部的
get
、put
、remove
包括trimToSize
都是安全的(因为都上锁了)。 - LruCache 自身并没有释放内存,将 LinkedHashMap 的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存。
- 覆写
entryRemoved
方法能知道 LruCache 数据移除是是否发生了冲突,也可以去手动释放资源。
*maxSize
和sizeOf(K key, V value)
方法的覆写息息相关,必须相同单位。( 比如maxSize
是8MB,自定义的sizeOf
计算每个数据大小的时候必须能算出与MB之间有联系的单位 )
- Android LRUCache原理及源码分析(阿里巴巴)
- android LRUCache源码分析
- Android LruCache 源码分析
- LruCache源码分析及思考
- Android SupportV4 LruCache源码分析
- 【Java源码分析】Android-LruCache源码分析
- Android中LruCache的源码分析
- Android LruCache 缓存 类 源码 注解 分析
- Android缓存源码分析(DiskLruCache,LruCache)
- Android之LruCache使用和源码分析
- LruCache 实现原理分析
- LruCache 实现原理分析
- LruCache原理分析
- LruCache原理分析整理
- LruCache源码分析
- Lrucache源码分析
- LruCache源码分析
- LruCache源码分析
- java常用类库---定时调度
- 45-不同的继承方式
- JIRA 知多少:聊一聊 Android Studio 、工作流相关设置
- SpringMVC的环境搭建
- 计算机网络面试题汇总(持续更新)
- Android LRUCache原理及源码分析(阿里巴巴)
- (递归查询)关于oracle树结构查询 展示 分组,查找父节点,查找子节点问题
- Java经典基础练习11-20
- 少走弯路的10条建议
- Incorrect string value: '\xE5\x9E\x9A\xE5\x9B\xBD...' for column 'YD_COMPANY
- linux格式文档在windows下缺少换行的解决方法
- Android 沉浸式状态栏的实现
- Hive内部表和外部表的区别详解
- erlang高性能网络库esockd的编译和使用(一)