面试官:如何实现一个LruCache,原理是什么?

当成一个 Map 用就可以了,只不过实现了 LRU 缓存策略。

使用的时候记住几点即可:

  • 1.(必填)你需要提供一个缓存容量作为构造参数。

  • 2.(必填) 覆写 sizeOf 方法 ,自定义设计一条数据放进来的容量计算,如果不覆写就无法预知数据的容量,不能保证缓存容量限定在最大容量以内。

  • 3.(选填) 覆写 entryRemoved 方法 ,你可以知道最少使用的缓存被清除时的数据( evicted, key, oldValue, newVaule )。

  • 4.(记住)LruCache是线程安全的,在内部的 get、put、remove 包括 trimToSize 都是安全的(因为都上锁了)。

  • 5.(选填) 还有就是覆写 create 方法 。

一般做到 1、2、3、4就足够了,5可以无视 。

以下是 一个 LruCache 实现 Bitmap 小缓存的案例, entryRemoved 里的自定义逻辑可以无视,想看的可以去到我的我的展示 demo 里的看自定义 entryRemoved 逻辑。

private static final float ONE_MIB = 1024 * 1024;

// 7MB

private static final int CACHE_SIZE = (int) (7 * ONE_MIB);

private LruCache<String, Bitmap> bitmapCache;

this.bitmapCache = new LruCache<String, Bitmap>(CACHE_SIZE) {

protected int sizeOf(String key, Bitmap value) {

return value.getByteCount();

}

@Override

protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {

}

};

3. 源码分析

3.1 LruCache 原理概要解析

LruCache 就是 利用 LinkedHashMap 的一个特性( accessOrder=true 基于访问顺序 )再加上对 LinkedHashMap 的数据操作上锁实现的缓存策略。

LruCache 的数据缓存是内存中的。

  • 1.首先设置了内部 LinkedHashMap 构造参数 accessOrder=true, 实现了数据排序按照访问顺序。

  • 2.然后在每次 LruCache.get(K key)`` 方法里都会调用LinkedHashMap.get(Object key)`。

  • 3.如上述设置了 accessOrder=true 后,每次 LinkedHashMap.get(Object key) 都会进行 LinkedHashMap.makeTail(LinkedEntry<K, V> e)

  • 4.LinkedHashMap 是双向循环链表,然后每次 LruCache.get -> LinkedHashMap.get 的数据就被放到最末尾了。

  • 5.在 put 和 trimToSize 的方法执行下,如果发生数据量移除,会优先移除掉最前面的数据(因为最新访问的数据在尾部)。

具体解析在:3.2、3.3、3.4、3.5 。

3.2 LruCache 的唯一构造方法

/**

  • LruCache的构造方法:需要传入最大缓存个数

*/

public LruCache(int maxSize) {

this.maxSize = maxSize;

/*

  • 初始化LinkedHashMap

  • 第一个参数:initialCapacity,初始大小

  • 第二个参数:loadFactor,负载因子=0.75f

  • 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序

*/

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 查询缓存,如果存在于缓存或者被 create 方法创建了。

  • 如果值返回了,那么它将被移动到双向循环链表的的尾部。

  • 如果如果没有缓存的值,则返回 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) 逻辑

  • 因为默认的 create(K key) 逻辑为null

*/

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 += safeSizeOf(key, createdValue);

}

}

// 如果上面 判断出了 将要放入的值发生冲突

if (mapValue != null) {

/*

  • 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了

  • 告诉 自定义 的 entryRemoved 方法

*/

entryRemoved(false, key, createdValue, mapValue);

return mapValue;

} else {

// 上面 进行了 size += 操作 所以这里要重整长度

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 of the mapping with the specified key.

  • @param key

  •        the key.
    
  • @return the value of the mapping with the specified key, or {@code null}

  •     if no mapping for the specified key is found.
    

*/

@Override public V get(Object key) {

/*

  • This method is overridden to eliminate the need for a polymorphic

  • invocation in superclass at the expense of code duplication.

*/

if (key == null) {

HashMapEntry<K, V> e = entryForNullKey;

if (e == null)

return null;

if (accessOrder)

makeTail((LinkedEntry<K, V>) e);

return e.value;

}

int hash = Collections.secondaryHash(key);

HashMapEntry<K, V>[] tab = table;

for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];

e != null; e = e.next) {

K eKey = e.key;

if (eKey == key || (e.hash == hash && key.equals(eKey))) {

if (accessOrder)

makeTail((LinkedEntry<K, V>) e);

return e.value;

}

}

return null;

}

其实仔细看 if (accessOrder) 的逻辑即可,如果 accessOrder=true 那么每次 get 都会执行 N 次 makeTail(LinkedEntry<K, V> e) 。

接下来看看:

3.5 LinkedHashMap.makeTail(LinkedEntry<K, V> e)

/**

  • Relinks the given entry to the tail of the list. Under access ordering,

  • this method is invoked whenever the value of a pre-existing entry is

  • read by Map.get or modified by Map.put.

*/

private void makeTail(LinkedEntry<K, V> e) {

// Unlink e

e.prv.nxt = e.nxt;

e.nxt.prv = e.prv;

// Relink e as tail

LinkedEntry<K, V> header = this.header;

LinkedEntry<K, V> oldTail = header.prv;

e.nxt = header;

e.prv = oldTail;

oldTail.nxt = header.prv = e;

modCount++;

}

// Unlink e

// Relink e as tail

LinkedHashMap 是双向循环链表,然后此次   LruCache.get -> LinkedHashMap.get   的数据就被放到最末尾了。

以上就是 LruCache 核心工作原理。

接下来介绍 LruCache 的容量溢出策略。

4.6 LruCache.put(K key, V value)

public final V put(K key, V value) {

synchronized (this) {

// 拿到键值对,计算出在容量中的相对长度,然后加上

size += safeSizeOf(key, value);

}

trimToSize(maxSize);

最后

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。

最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
点击传送门,即可获取!

省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

[外链图片转存中…(img-Cap2jewZ-1715429309808)]

【算法合集】

[外链图片转存中…(img-Gr4cub20-1715429309809)]

【延伸Android必备知识点】

[外链图片转存中…(img-rBoyQuTC-1715429309810)]

【Android部分高级架构视频学习资源】

Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值