Android面试题(22)-lruCache与DiskLruCache缓存详解

本文详细介绍了Android中LruCache和DiskLruCache的缓存原理,包括HashMap、LinkedHashMap的基础知识,以及LruCache如何实现内存缓存,DiskLruCache如何进行硬盘缓存。讲解了两者如何利用LinkedHashMap实现LRU算法,以及在Android应用中的使用和配置。
摘要由CSDN通过智能技术生成

关于lruCache(最近最少使用)的算法,这是一个比较重要的算法,它的应用非常广泛,不仅仅在Android中使用,Linux系统等其他地方中也有使用;今天就来看一看这其中的奥秘;

讲到LruCache,就不得不讲一讲LinkedHashMap,而对于LinkedHashMap,它继承的是HashMap,那么我们就先从HashMap开始看起吧;

注:此篇博客所讲的所有知识都是在jdk1.8环境下的,java8的hashmap相比之前的版本又做了一层优化,当链表过长时(默认超过8),会改为采用红黑树这种自平衡的数据结构去进行存储优化

HashMap

我们知道,数据结构中的存在两种常见的存储结构,一个是数组,一个是链表;两者各有优劣,首先数组的存储空间在内存中是连续的,这就就导致占用内存严重,连续的大内存进入老年代的可能性也会变大,但是正因为如此,寻址就显得简单,也就是说查询某个arr会有指定的下标,但是插入和删除比较困难,因为每次插入和删除时,如果数组在插入这个地方后面还有很多数据,那就要后面的数据整体往前或者往后移动。对于链表来说存储空间是不连续的,占用内存比较宽松,它的基本结构是一个节点(node)都会包含下一个节点的信息(如果是双向链表会存在两个信息一个指向上一个一个指向下一个),正因为如此寻址就会变得比较困难,插入和删除就显得容易,链表插入和删除的时候只需要修改节点指向信息就可以了。

那么两者各有优劣,将它们两者结合起来会有什么效果呢?自然早就有大神尝试过了,并且尝试的很成功,它的产物就是HashMap哈希表,也叫散列表;

HashMap的主干是一个数组,里面存储的是一个个的Node,Node中包含了哈希值,key,value和下一个Node的引用;

Node(int hash, K key, V value, Node<K,V> next) {
    this.hash = hash;
    this.key = key;
    this.value = value;
    this.next = next;
}
存储在HashMap中的每一个值都需要一个key,这是为什么呢?这个问题可以再问细一点,hashmap是如何存放数据的?

我们先来看看他的一些基本属性:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

这个属性表示HashMap的初始容量大小是16;

static final int MAXIMUM_CAPACITY = 1 << 30;

最大容量为2^30;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

这个表示加载因子默认为0.75,代表hashmap的填充程度,加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。

反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)

冲突的机会越大,则查找的成本越高.

因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.

  如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它默认为0.75就可以了;

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;
临界值,这个字段主要是用于当HashMap的size大于它的时候,需要触发resize()方法进行扩容

构造方法:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

可以清晰的看到当new一个HashMap时,并没有为数组分配内存空间(有一个传入map参数的构造方法除外);

几个核心方法:

put方法实际调用的就是putVal方法,所以我们先看putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

这里的逻辑由于是java8,所以会复杂一点,里面有几个关键点,记录下来,对比着源码看:

(1)putVal方法其实就可以理解为put方法,我们使用hashmap的时候,什么时候才会使用put方法呢,当你想要存储数据的时候会调用,那么putVal方法的逻辑就是为了把你需要存储的数据按位置存放好就可以了;

(2)具体的存放逻辑是通过复杂的if判断来完成的,首先会判断当前通过key和hash函数计算出的数组下标位置的是否为null,如果是空,直接将Node对象存进去;如果不为空,那么就将key值与桶中的Node的key一一比较,在比较的过程中,如果桶中的对象是由红黑树构造而来,那么就使用红黑树的方法去进行存储,如果不是,那么就继续判断当前桶中的元素是否大于8,大于8的话就使用红黑树处理(调用treeifybin方法),如果小于8,那么进行最后的判断是否key值相同,如果相同,就直接将旧的node对象替换为新的node对象;这样就保证了存储的正确性;

(3)在putVal中有这么一句

++modCount;

这里的modCount的作用是用来判断当前HashMap是否在由一个线程操作,因为hashmap本身是线程不安全的,多线程操作会造成其中数据不安全等多种问题,modcount记录的是put的次数,如果modcount不等于put的node的个数的话,就代表有多个线程同时操作,就会报ConcurrentModificationException异常;

再来看看get方法,get方法其实调用的是getNode方法

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值