本人只是 Android小菜一个,写技术文档只是为了总结自己在最近学习到的知识,从来不敢为人师,如果里面有些不正确的地方请大家尽情指出,谢谢!

1. 概述

由于 Android 为每个进程分配的可用内存空间都是有限的,如果进程使用的内存超过了所分配的限制就会出现内存溢出问题。同时,如果应用每使用一个资源都需要从本地或网络加载,这无疑会影响应用的性能,为了既能保证应用性能又能避免内存溢出,就出现内存缓存技术

所谓内存缓存技术指的是把一些资源缓存在内存中,如果需要加载资源,首先到内存中去寻找,寻找到的话就直接使用,否则去本地或者网络去寻找。其中最重要的是内存缓存技术要有一个合适的缓存策略,即根据什么策略把缓存中的资源删除,以保证缓存空间始终在一个合理的范围内。

LruCacheAndroid提供的一个标准的基于LRU,最近最少使用算法的缓存技术,它的使用方法已经在其他博文里简单介绍过了,这里主要介绍它的实现机制。

2. LruChche 实现原理

LRU的全称是Least Recently Used,最近最少使用LruCache的实现原理就是在其内部维护一个队列,内部元素按照最近使用时间进行排序,队首是最近最常使用的元素,队尾是最近最少使用的元素,当缓存中元素达到最大数量后,把最近最少使用的元素即队尾元素从缓存队列中移除,从而保证缓存始终在一个合理内存范围内。

下图简单演示LruCache的过程:

从这个演示图中可以发现:

  1. 每次新入队的元素总是位于队首;
  2. 队尾元素是最久没有使用过的元素;
  3. 当队列中的元素被再次使用后,就会把该元素重新插入到队首。

LruCache中使用LinkedHashMap来保存元素,而 LinkedHashMap内部使用双向链表来实现这样的一个 LRU队列,其具体实现在这里就不详细描述了,大家只要了解这点就可以了。

3. LruCache 关键实现

内存缓存技术中最关键的实现主要包含三部分:

  • 如何把元素加入缓存
  • 如何从缓存中获取元素
  • 如何在缓存满时删除元素

3.1 LruCache 的初始化

在详细讲解LruCache的三个关键实现部分前,首先要知道LruCache 的初始化。 首先看下是如何在代码里使用LruCache的:

    int maxMemory = (int) Runtime.getRuntime().maxMemory();LruCache<String, Bitmap> mCache = new LruCache<String, Bitmap>(maxMemory / 4) {@Overrideprotected int sizeOf(String key, Bitmap value) {return value.getByteCount();}};
复制代码

在这段示例代码里,创建了一个LruCache示例并重写了sizeOf方法。重写sizeOf方法是因为它会被用来判断缓存的当前大小是否已经达到了预定义的缓存大小,如果超过就需要从中移除最久没有使用的元素。默认情况下sizeOf返回的时候元素个数,所以如果在创建LruCache时指定的缓存中的元素个数而非内存空间就可以不重新sizeOf方法。

现在来看在创建LruCache的时候到底发生了什么,其构造函数如下:

    /*** @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并把其中的第三个参数设置为trueLinkedHashMap的构造函数如下:

    /*** Constructs an empty <tt>LinkedHashMap</tt> instance with the* specified initial capacity, load factor and ordering mode.** @param  initialCapacity the initial capacity* @param  loadFactor      the load factor* @param  accessOrder     the ordering mode - <tt>true</tt> for*         access-order, <tt>false</tt> for insertion-order* @throws IllegalArgumentException if the initial capacity is negative*         or the load factor is nonpositive*/public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {super(initialCapacity, loadFactor);this.accessOrder = accessOrder;}
复制代码

其中,参数分别是初始容量, 负载因子和排序方式,如果accessOrder被设置为true就表示是按照访顺序进行排序的,这也就保证了LruCache中的原生是按照访问顺序排序的。

所以在LruCache的初始化过程中,一方面确定了缓存的最大空间,另一方面利用LinkedHashMap实现了LRU队列。

3.2 LruCache 缓存元素

要使用LruCache,首先需要把需要缓存的资源加入到LruCache缓存空间,在LruCache实现这一功能的是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++;// 更新当前缓存大小并把元素加入缓存队列,新元素位于队首。size += safeSizeOf(key, value);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;}
复制代码

put方法整体逻辑比较简单,就是把新元素放在队首,更新当前缓存大小,并使用trimToSize 来保证当前缓存大小没有超过限制,其代码如下:

    /*** @param maxSize the maximum size of the cache before returning. May be -1*     to evict even 0-sized elements.*/private 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;}// BEGIN LAYOUTLIB CHANGE// get the last item in the linked list.// This is not efficient, the goal here is to minimize the changes// compared to the platform version.Map.Entry<K, V> toEvict = null;for (Map.Entry<K, V> entry : map.entrySet()) {toEvict = entry;}// END LAYOUTLIB CHANGEif (toEvict == null) {break;}// 找到对稳元素,即最久没有使用的元素,并移除之。key = toEvict.getKey();value = toEvict.getValue();map.remove(key);// 移除元素后更新当前大小size -= safeSizeOf(key, value);evictionCount++;}entryRemoved(true, key, value, null);}}
复制代码

trimToSize的逻辑也很简单明了,在缓存队列中找到最近最久没有使用的元素,把它从队列中移除,直到缓存大小满足限制。由于最近最久没有使用的元素一直位于队尾,所以只要找到队尾元素并把它移除即可。

3.3 LruCache 取元素

缓存元素的最终目的是为了方便后续能从缓存中更快地获取需要元素,LruCache获取元素是通过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.*/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.*/// 如果找不到元素就调用 create 去创建一个元素,默认 create 返回 null.V createdValue = create(key);if (createdValue == null) {return null;}synchronized (this) {createCount++;mapValue = map.put(key, createdValue);// 新创建的元素和队列中已存在元素冲突,这个已存在元素是在 create的过程中新加入队列的。if (mapValue != null) {// There was a conflict so undo that last putmap.put(key, mapValue);} else {// 加入新创建元素后需要更新缓存大小size += safeSizeOf(key, createdValue);}}if (mapValue != null) {entryRemoved(false, key, createdValue, mapValue);return mapValue;} else {// 检查缓存空间trimToSize(maxSize);return createdValue;}}
复制代码

get方法的逻辑也是很简洁明了的,就是直接从缓存队列中获取元素,如果查找到就返回并更新元素位置到队首,如果查不到就自己创建一个加入队列,但考虑到多线程的情况,加入队列是需要考虑冲突情况。

3.4 LruCache 移除元素

虽然LruCache可以在缓存空间达到限制是自动把最近最久没使用的元素从队列中移除,但也可以主动去移除元素,使用的方法就是remove,其代码如下:

    /*** Removes the entry for {@code key} if it exists.** @return the previous value mapped by {@code key}.*/public final V remove(K key) {if (key == null) {throw new NullPointerException("key == null");}V previous;synchronized (this) {// 找到元素后移除,并更新缓存大小。previous = map.remove(key);if (previous != null) {size -= safeSizeOf(key, previous);}}if (previous != null) {entryRemoved(false, key, previous, null);}return previous;}
复制代码

remove的逻辑更加简单,到缓存队列中找到元素,移除,并更新缓存大小即可。

4. 总结

本文主要分析了LruCache的内部实现机制,由于LruCache本身的代码量比较小,分析起来难度也不大,但养成分析源码的习惯所代表的意义更大,让我们一起 Reading The Fucking Source Code !

理解 LruCache 机制相关推荐

  1. 一起谈.NET技术,.Net Discovery系列之-深入理解平台机制与性能影响 (中)

    上一篇文章中Aicken为大家介绍了.Net平台的垃圾回收机制与其对性能的影响,这一篇中将继续为大家介绍.Net平台的另一批黑马-JIT.有关JIT的机制分析 ● 机制分析以C#为例,在C#代码运行前 ...

  2. 第二十节: 深入理解并发机制以及解决方案(锁机制、EF自有机制、队列模式等)

    一. 理解并发机制 1. 什么是并发,并发与多线程有什么关系? ①. 先从广义上来说,或者从实际场景上来说. 高并发通常是海量用户同时访问(比如:12306买票.淘宝的双十一抢购),如果把一个用户看做 ...

  3. 深入理解类加载机制:拨开迷雾见真章

    Java语言将封装性表现的淋漓尽致,程序员在写Java代码的时候根本不用考虑自己写的代码在后期运行时是如何被JVM加载到内存中的,但是想告别CRUD,进阶为一名高级程序员的话,JVM的类加载机制必须了 ...

  4. 【MySQL进阶-05】深入理解mvcc机制(详解)

    MySql系列整体栏目 内容 链接地址 [一]深入理解mysql索引本质 https://blog.csdn.net/zhenghuishengq/article/details/121027025 ...

  5. 深入理解attention机制

    深入理解attention机制 1. 前言 2. attention机制的产生 3. attention机制的发展 4. attention机制的原理 5. attention的应用 参考文献 1. ...

  6. 干货|理解attention机制本质及self-attention

    点击上方"小白学视觉",选择加"星标"或"置顶"重磅干货,第一时间送达 上一篇,我们讲述了attention的知识,这篇接上篇,更加深入的理 ...

  7. .Net Discovery系列之十一-深入理解平台机制与性能影响 (中)

    上一篇文章中Aicken为大家介绍了.Net平台的垃圾回收机制与其对性能的影响,这一篇中将继续为大家介绍.Net平台的另一批黑马-JIT.    有关JIT的机制分析    ● 机制分析    以C# ...

  8. 理解注意力机制的好文二

    注意力模型最近几年在深度学习各个领域被广泛使用,无论是图像处理.语音识别还是自然语言处理的各种不同类型的任务中,都很容易遇到注意力模型的身影.所以,了解注意力机制的工作原理对于关注深度学习技术发展的技 ...

  9. .Net Discovery系列之十二-深入理解平台机制与性能影响(下)

    上一篇文章中Aicken为大家介绍了.Net平台的垃圾回收机制.即时编译机制与其对性能的影响,这一篇中将继续为大家介绍.Net平台的异常捕获机制与字符串驻留机制. 三.关于异常捕获机制 虽然我们已经很 ...

最新文章

  1. Oracle中的substr()函数 详解及应用
  2. [转]ASP.NET MVC4中@model使用多个类型实例的方法
  3. 亲身试验 pycharm 下载 并 安装 pygame包
  4. MyBatis拦截器原理探究MyBatis拦截器原理探究 1
  5. 从 0 开始机器学习 - 机器学习系统的设计与误差分析
  6. CMD如何进入C:WINDOWS\SYSTEM32
  7. PLC与工业DTU接线快速入门
  8. 基于单片机的红外光电计数器(双探头)
  9. java excel 冻结_poi excel 常用操作 [冻结、合并、链接]
  10. 向传奇致敬,向约翰·纳什和他的妻子艾丽西亚致敬,缔造了数学和爱的传奇
  11. 新形势下安全风险评估实践
  12. LeetCode12. 整数转罗马数字 / 剑指 Offer 40. 最小的k个数 / 剑指 Offer 41. 数据流中的中位数
  13. body与html 会有间隙,css – thead和tbody之间的间距
  14. 自定义ImageView实现播放帧动画
  15. KCF算法(相关滤波算法) 跟踪目标
  16. 地铁框架保护的原理_浅析地铁直流框架保护原理及应急处置
  17. ruby 数组自定义排序_在Ruby中对数组排序
  18. echarts入门(详细教程)
  19. Java精品项目源码第117期超市收银管理系统
  20. 100个国家的数字货币政策大全

热门文章

  1. 图像处理之形态学梯度计算
  2. Powershell都有哪些好用的技能?
  3. 关于波峰波谷趋势分割(想象中的方法),判断趋势,突然来想到的,记下来,没有实验。以便以后用于分割...
  4. sqlserver 批量删除相同前缀名的表
  5. WinForm员工信息表
  6. 二、网络编程中的常用类
  7. Error(1.0.5 1107071739): D:\SAE_SDK_Windows_1.0...
  8. oracle-审计3
  9. (原創) 如何為Blog加上簡體中文(繁體中文)翻譯? (Web) (CSS) (JavaScript)
  10. redis批量删除指定的key