LruCache基本使用和原理分析

2023-11-07

最近在研究时区问题时,时区的底层实现涉及到BasicLruCache集合的使用,故而对LruCache做了部分的了解,BasicLruCache 是 Android 提供的一个简单的 LRU 缓存实现,但在标准的 Java 类库中并不存在。

目录

  • LruCache基本原理
  • LruCache的基本使用
  • LruCache主要源码解析
    构造方法
    添加数据put
    检测trimToSize
    查询方法get
  • 总结

一. LruCache基本原理

LRU全称为Least Recently Used,即最近最少使用。

LRU算法就是当缓存空间满了的时候,将最近最少使用的数据从缓存空间中删除,以增加可用的缓存空间来缓存新数据。

这个算法的内部有一个缓存列表,每当一个缓存数据被访问的时候,这个数据就会被提到列表尾部,每次都这样的话,列表的头部数据就是最近最不常使用的了,当缓存空间不足时,就会删除列表头部的缓存数据。

二. LruCache的基本使用

常用方法主要就是put接口以及get接口。demo代码:

import android.util.LruCache;

public class Main {
    public static void main(String[] args) {
        // 创建一个容量为 3 的 LRU 缓存
        LruCache<String, Integer> cache = new LruCache<>(3);
        
        // 向缓存中添加键值对
        cache.put("key1", 1);
        cache.put("key2", 2);
        cache.put("key3", 3);
        
        // 遍历并打印缓存中的所有键值对
        for (String key : cache.snapshot().keySet()) {
            Integer value = cache.get(key);
            System.out.println(key + " : " + value);
        }
    }
}

代码输出结果:

key3 : 3
key2 : 2
key1 : 1

三. LruCache主要源码解析

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

LinkedHashMap默认的构造参数是默认 插入顺序的,就是说你插入的是什么顺序,读出来的就是什么顺序,但是也有访问顺序,就是说你访问了一个key,这个key就跑到了最后面。

LruCache 的数据缓存是内存中的

  • 首先设置了内部 LinkedHashMap 构造参数 accessOrder=true, 实现了数据按照访问顺序排序。
  • LruCache类在调用get(K key) 方法时,都会调用LinkedHashMap.get(Object key) 。设置了 accessOrder=true 后,调用LinkedHashMap.get(Object key) 都会通过LinkedHashMap的afterNodeAccess()方法将数据移到队尾。
  • 由于最新访问的数据在尾部,在 put 和 trimToSize 的方法执行下,如果发生数据移除,会优先移除掉头部数据

1.构造方法

   /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

LinkedHashMap参数介绍:

  • initialCapacity 用于初始化该 LinkedHashMap 的大小。
  • loadFactor(负载因子)这个LinkedHashMap的父类 HashMap 里的构造参数,涉及到扩容问题,比如 HashMap 的最大容量是100,那么这里设置0.75f的话,到75的时候就会扩容。
  • accessOrder,这个参数是排序模式,true表示在访问的时候进行排序( LruCache 核心工作原理就在此),false表示在插入的时才排序。

2.添加数据put

/**
     * 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++;
             //safeSizeOf(key, value)。
             //这个方法返回的是1,也就是将缓存的个数加1.
            // 当缓存的是图片的时候,这个size应该表示图片占用的内存的大小,所以应该重写里面调用的sizeOf(key, value)方法
            size += safeSizeOf(key, value);
            //向map中加入缓存对象,若缓存中已存在,返回已有的值,否则执行插入新的数据,并返回null
            previous = map.put(key, value);
            //如果已有缓存对象,则缓存大小恢复到之前
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }
          //entryRemoved()是个空方法,可以自行实现
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
 
        trimToSize(maxSize);
        return previous;
    }

开始的时候确实是把值放入LinkedHashMap,不管超不超过你设定的缓存容量。
根据 safeSizeOf方法计算 此次添加数据的容量是多少,并且加到size 里 。
方法执行到最后时,通过trimToSize()方法 来判断size 是否大于maxSize。
可以看到put()方法并没有太多的逻辑,重要的就是在添加过缓存对象后,调用 trimToSize()方法,来判断缓存是否已满,如果满了就要删除近期最少使用的数据。

3.检测trimToSize

/**
     * 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.
     */
    public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
              //如果map为空并且缓存size不等于0或者缓存size小于0,抛出异常
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }
     //如果缓存大小size小于最大缓存,不需要再删除缓存对象,跳出循环
                if (size <= maxSize) {
                    break;
                }
     //在缓存队列中查找最近最少使用的元素,若不存在,直接退出循环,若存在则直接在map中删除。
                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }
 
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                //回收次数+1
                evictionCount++;
            }
 
            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;
}

trimToSize()方法不断地删除LinkedHashMap中队首的元素,即近期最少访问的,直到缓存大小小于最大值。

4.查询方法get

    /**
     * Returns the value for {@code key} if it exists in the cache or can be
     * created by {@code #create}. If a value was returned, it is moved to the
     * head of the queue. This returns null if a value is not cached and cannot
     * be created.
     * 通过key获取缓存的数据,如果通过这个方法得到的需要的元素,那么这个元素会被放在缓存队列的尾部,
     * 
     */
    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }
 
        V mapValue;
        synchronized (this) {
              //从LinkedHashMap中获取数据。
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }
    /*
     * 正常情况走不到下面
     * 因为默认的 create(K key) 逻辑为null
     * 走到这里的话说明实现了自定义的create(K key) 逻辑,比如返回了一个不为空的默认值
     */
        /*
         * 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.
         * 译:如果通过key从缓存集合中获取不到缓存数据,就尝试使用creat(key)方法创造一个新数据。
         * create(key)默认返回的也是null,需要的时候可以重写这个方法。
         */
 
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }
        //如果重写了create(key)方法,创建了新的数据,就讲新数据放入缓存中。
        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的get()方法获取集合中的缓存对象时,就代表访问了一次该元素,将会更新队列,保持整个队列是按照访问顺序排序,这个更新过程就是在LinkedHashMap中的get()方法中完成的。

总结

LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。
当调用put()方法时,就会在集合中添加元素,并调用trimToSize()判断缓存是否已满,如果满了就用LinkedHashMap的迭代器删除队首元素,即近期最少访问的元素。
当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队尾。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

LruCache基本使用和原理分析 的相关文章

  • Java ZIP - 如何解压缩文件夹?

    是否有任何示例代码 如何将 ZIP 中的文件夹部分解压到我想要的目录中 我已将文件夹 FOLDER 中的所有文件读取到字节数组中 如何从其文件结构创建 我不确定你所说的部分是什么意思 您的意思是在没有 API 帮助的情况下自己完成吗 如果您
  • 迭代 Sqlite-query 中的行

    我有一个表布局 我想用数据库查询的结果填充它 我使用全选 查询返回四行数据 我使用此代码来填充表行内的 TextView Cursor c null c dh getAlternative2 startManagingCursor c th
  • Kotlin 协程阻塞 Android 中的主线程

    我是 Kotlin 和协程的新手 我有一个fun在我的活动及其内部 检查User用户名和密码 如果为真 则返回Users object 一切都好 但是当我按下按钮时 我的活动被阻止并等待响应Users login 我用这个有趣的 priva
  • 改造添加带有令牌和 ID 的标头

    我在获取经过身份验证的用户时遇到问题 在此之前我得到了令牌和用户 ID 现在我需要使用访问令牌和 ID 从服务器获取用户 我有标题格式 https i stack imgur com OQ87Y png 现在我尝试使用拦截器添加带有用户令牌
  • 温度转换 2 字节

    我很难转换两个字节的温度 我有一个控制单元 温度传感器 我可以在其中获取两个字节的温度消息 1 示例 message 40 25 LSBYTE 40 MSBYTE 25 0 03125 C bit temperature 25C seen
  • Android Studio 3.0 - 设置未保存

    我已将 文件 gt 设置 gt 编辑器 gt 代码样式 中的 右边距 列 从默认的 100 增加到 140 不幸的是 每次重新启动 Android Studio 后 该边距都会重置 我还尝试导出和导入我的设置 但这并不能阻止重置右边距 希望
  • 改变换行行为

    我可以在 TextView 中使用 Spannable 创建具有不同外观 下划线 删除线等的跨度 我怎样才能做同样的事情来改变换行行为 特别是 我不希望电子邮件地址在中间换行 我希望它像一个单词一样 I tried 包裹在一起跨度 http
  • IllegalStateException:无法更改片段的标签,以前是 android:switcher,现在是 android:switcher

    我的活动使用TabLayout ViewPager 这里的选项卡和页面的数量是动态的 具体取决于从服务器获取的数据 崩溃是通过 Crashlytics 报告的 我无法复制它 我的活动代码 Override protected void on
  • 如何在 Google 地图中创建自定义地图?

    我正在尝试创建一个包含我家地图的 Google 地图应用程序 卧室 浴室 厨房等 使用 GPS 我会找到我现在在家里的位置 并尝试获取到我卧室的方向 步行距离 您可以使用Google的API来获取方向 我需要知道的是 如何添加我家的自定义地
  • 重构 google 的 NetworkBoundResource 类以使用 RxJava 而不是 LiveData

    谷歌的android架构组件教程here https developer android com topic libraries architecture guide html有一部分解释了如何抽象通过网络获取数据的逻辑 在其中 他们使用
  • 为什么我们在同一台服务器上使用多个应用程序服务器实例

    我想这是有充分理由的 但我不明白为什么有时我们会在同一物理服务器上放置例如 5 个具有相同 Web 应用程序的实例 这与多处理器架构的优化有关吗 JVM 或其他允许的最大内存限制 嗯 过了很长一段时间我又看到这个问题了 一台机器上的多个 J
  • java.lang.NoClassDefFoundError: org/apache/commons/cli/ParseException

    我想将 apache cli 添加到我的应用程序中 但我有问题 当我尝试运行它时显示这些错误 Error A JNI error has occurred please check your installation and try aga
  • 为什么ArrayList的非静态内部类SubList有一个成员变量“parent”?

    java util ArrayList SubList 是 java util ArrayList 的非静态内部类 这意味着它保存对其封闭类的引用 我们可以使用ArrayList this来访问java util ArrayList的成员
  • java.lang.OutOfMemoryError:尝试将 Java 对象转换为 Json 字符串时的 Java 堆空间

    我尝试将 csv 文件转换为 200K 对象的 Json 文件 其中对象代表 csv 中的 1 行 我在 32 位上安装了 Java 并且项目配置 VM 参数 Xmx1024m 但是我得到 Exception in thread main
  • 膨胀类 android.support.design.internal.BottomNavigationView 时出错

    我正在制作我的第一个应用程序 这是一个简单的应用程序 带有启动屏幕和主要活动 现在我有两个构建变体 免费版本 活动底部有 Admob 横幅 付费版本 该应用程序不会在底部显示 admob 横幅 而是将其替换为用于切换活动的底部导航视图 我将
  • 没有 Google Play 服务的设备的后备计划是什么

    目前 我正在将以前使用 jar 库的 Google 服务迁移到 Google Play 服务 谷歌广告移动 谷歌分析 谷歌云端硬盘 然而 在迁移指南中 Google 没有提到对于没有 Google Play 服务或没有最新的 Google
  • OnSwipe 方法在 RecyclerView 中不起作用

    我正在开发一个用于播放音频文件的应用程序 创建了包含 2 个选项卡的选项卡布局 两者中都使用了片段RecyclerView两者都被使用 该片段名为LibraryFragment有这个RecycleView其物品在刷卡时必须传递给HomeFr
  • JAXB 枚举字段未序列化

    我有以下课程 package dictionary import java io Serializable import java util Objects import javax xml bind annotation XmlEleme
  • POJO 支持使用omnifaces 自动完成primefaces

    我正在尝试在我的项目中使用 primefaces 自动完成组件 以避免将特定转换器写入我尝试使用的每个列表对象全能面孔 http showcase omnifaces org converters ListConverter如建议的here
  • Java:将秒转换为分钟、小时和天[重复]

    这个问题在这里已经有答案了 任务是 输出应如下所示 最好回显输入 您输入了 500 000 秒 即 5 天 18 小时 53 分钟 20 秒 5天18 53 20小时 我该怎么做呢 最容易理解和做到的方法是什么 讲师还说 没有硬编码 我不太

随机推荐

  • 关于虚拟机时不时繁忙、黑屏的解决方法(适用于VM12版本)

    关于虚拟机时不时黑屏 繁忙 关不了机的解决方法 针对于出现启动集群多个节点时 某一节点黑屏的情况 或者是单单要启动一台虚拟机时黑屏的情况 我个人用的是vm12版本 该方法可能不适用于VM15 我也没有试验过 不知道是否管用 特别是在启动集群
  • idea快捷键--编码

    1 创建main方法 psvm 输入public static void main的首字母 psvm 然后按tab或者enter 就会写好main方法 2 输出语句 System out print 键入 sout 3 for循环 输入 f
  • GitLab 与 Gerrit

    origin http blog csdn net feng8888bbb article details 70170638 相信大家看到这里 会发现gitlab比gerrit多了许多功能 比如说Issues Wiki等 我们从几个方面对比
  • string类常用函数

    1 substr函数 字符串截取函数 用于获取字符串的子串 str substr begin length 用于截取str中以begin为下标长度为length的字串 string s asd s s substr 0 1 结果为a 2 f
  • 逆变器和Modbus浅理解

    最近有兄弟去了能源部门 刚好跟着学了一些相关的知识 撇在这记录一下 当然理解的可能不是很正确 尤其Modbus协议压根没有写过 轻喷
  • php 怎么接受流数据类型_PHP数据类型

    PHP 支持 9 种原始数据类型 四种标量类型 boolean 布尔型 integer 整型 float 浮点型 也称作double string 字符串 三种复合类型 array 数组 object 对象 callable 可调用 两种特
  • linux 根目录各目录功能简介

    CentOS 6 5 目录 说明 备注 bin 存放普通用户可执行的指令 即使在单用户模式下也能够执行处理 boot 开机引导目录 包括Linux内核文件与开机所需要的文件 dev 设备目录 所有的硬件设备及周边均放置在这个设备目录中 et
  • Docker Compose 笔记 网络

    Compose 简介 1 引入 通过前面的知识 我们知道使用一个 Dockerfile 模板文件 可以让用户很方便的定义一个单独的应用容器 从而得到一个镜像 然而 在日常工作中 经常会碰到需要多个容器相互配合来完成某项任务的情况 例如要实现
  • xss挑战1-10关

    level1 没有任何过滤 直接URL后面加XSS测试语句 弹窗 level2 有搜索框 像第一关一样在搜索框测试一下 但是没有像第一关一样弹窗 查看源码发现我们的XSS语句被赋值给value并且在input标签里 所以我们需要闭合valu
  • Acwing-4644. 求和

    暴力解法 TLE了hh include
  • git入门教程

    文章目录 Git教程 创建版本库 把文件添加到版本库 查看文件状态 版本回退 工作区和暂存区 管理 撤销修改 删除文件 Git远程仓库 Git教程 Git是目前世界上最先进的分布式版本控制系统 Git用C语言开发 Linus一直痛恨的CVS
  • Midjourney关键字--兔子类

    1 A lovely and happy Pixar style rabbit baby wearing a checked shirt carrying a schoolbag to school with a sweet smile b
  • zkEVM战局简析:zkSync、StarkNet、Scroll和挑战者们

    不同的项目正在探索不同的方向 这或许是最利于行业的发展模式 原文作者 Grant Griffith 由 Odaily星球日报 Azuma 编译 编者按 10 月 28 日 由 Matter Labs 构建的以太坊扩容解决方案正式发布了 zk
  • JAVA之猜数字游戏

    1 随机生成一个0 99 包括0和99 的数字 从控制台输入猜测的数字 输出提示太大还是太小 继续猜测 直到猜到为止 游戏过程中 记录猜对所需的次数 游戏结束后公布结果 打开记事本 写如下一段代码 import java util Rand
  • SpringBoot2对接线程池

    SpringBoot2对接线程池 1 配置线程池Bean package com itennishy test config import java util concurrent ThreadPoolExecutor import org
  • Qt中extern全局变量的使用

    参考网址 https blog csdn net Soar dream article details 101025153 https blog csdn net weixin 45571586 article details 118109
  • C语言位操作

    C语言位操作 1 位与 按照二进制位一一对应 只有全1才为真 否则为假 特定位置置0用位与 2 位或 按照二进制 有真则真 全假才假 特定位置置1用位或 3 位取反 按照二进制位 1变为0 0变为1 逻辑取反 数当成整体 不变二进制 不是0
  • 错误 Angular2 Can't bind to 'routerLink' since it isn't a known property of 'a'

    Can t bind to routerLink since it isn t a known property of a 解决办法 检查是否没有 import RouterModule import RouterModule from a
  • 阿里java编程规范之异常处理、安全规约、MySql数据库

    注 本文内容整理自 阿里java编码规范 除 编程规约 外的其它规则 异常处理 强制 1 Java类库中可以通过预检查方式规避的 RuntimeException不应该通过catch的方式来处理 如 IndexOutOfBoundsExce
  • LruCache基本使用和原理分析

    最近在研究时区问题时 时区的底层实现涉及到BasicLruCache集合的使用 故而对LruCache做了部分的了解 BasicLruCache 是 Android 提供的一个简单的 LRU 缓存实现 但在标准的 Java 类库中并不存在