【源码】LruCache源码剖析

上一篇分析了LinkedHashMap源码,这个Map集合除了拥有HashMap的大部分特性之外,还拥有链表的特点,即可以保持遍历顺序与插入顺序一致。另外,当我们将accessOrder设置为true时,可以使遍历顺序和访问顺序一致,其内部双向链表将会按照近期最少访问到近期最多访问的顺序排列Entry对象,这可以用来做缓存。

这篇文章分析的LruCache并不是jdk中的类,而是来自安卓,熟悉安卓内存缓存的必然对这个类不陌生。

LruCache内部维护的就是一个LinkedHashMap。

下面开始分析LruCache。

注:下面LruCache源码来自support.v4包。

首先是这个类的成员变量:

 private final LinkedHashMap<K, V> map;
    /** Size of this cache in units. Not necessarily the number of elements. */
    private int size;//当前大小
    private int maxSize;//最大容量
    private int putCount;//put次数
    private int createCount;//create次数
    private int evictionCount;//回收次数
    private int hitCount;//命中次数
    private int missCount;//丢失次数

LinkedHashMap的初始化放在构造器中:

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的accessOrder设置为true。

接下来看两个最重要的方法,put和get。首先是put方法:

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) {//之前已经插入过相同的key
                size -= safeSizeOf(key, previous);//那么减去该entry的容量,因为发生覆盖
            }
        }
        if (previous != null) {
            entryRemoved(false, key, previous, value);//这个方法默认空实现
        }
        trimToSize(maxSize);//若容量超过maxsize,将会删除最近很少访问的entry
        return previous;
    }

put方法无非就是调用LinkedHashMap的put方法,但是这里在调用LinkedHashMap的put方法之前,判断了key和value是否为空,也就是说LruCache不允许空键值。除此之外,put操作被加锁了,所以是线程安全的

既然是缓存,那么必然能够动态删除一些不常用的键值对,这个工作是由trimToSize方法完成的:

 public void trimToSize(int maxSize) {
        while (true) {//不断删除linkedHashMap头部entry,也就是最近最少访问的条目,直到size小于最大容量
            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 || map.isEmpty()) {//直到容量小于最大容量为止
                    break;
                }
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();//指向链表头
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);//删除最少访问的entry
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
            entryRemoved(true, key, value, null);
        }
    }

这个方法不断循环删除链表首部元素,也就是最近最少访问的元素,直到容量不超过预先定义的最大值为止。

注:LruCache在android.util包中也有一个LruCache类,但是我发现这个类的trimToSize方法是错误的:

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;
                }

                Map.Entry<K, V> toEvict = null;
                for (Map.Entry<K, V> entry : map.entrySet()) {
                    toEvict = entry;
                }

                if (toEvict == null) {
                    break;
                }
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
            entryRemoved(true, key, value, null);
        }
    }

这里的代码将会循环删除链表尾部,也就是最近访问最多的元素,这是不正确的!所以大家在做内存缓存的时候一定要注意,看trimToSize方法是否有问题。

接下来是get方法:

public final V get(K key) {
        if (key == null) {//不允许空键
            throw new NullPointerException("key == null");
        }
        V mapValue;
        synchronized (this) {//线程安全
            mapValue = map.get(key);//调用LinkedHashMap的get方法
            if (mapValue != null) {
                hitCount++;//命中次数加1
                return mapValue;//返回value
            }
            missCount++;//未命中
        }

        V createdValue = create(key);//默认返回为false
        if (createdValue == null) {
            return null;
        }
        synchronized (this) {
            createCount++;//如果创建成功,那么create次数加1
            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;
        }
    }

get方法即根据key在LinkedHashMap中寻找对应的value,此方法也是线程安全的。

以上就是LruCache最重要的部分,下面再看下其他方法:

remove:

  public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }
        V previous;
        synchronized (this) {
            previous = map.remove(key);//调用LinkedHashMap的remove方法
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }
        if (previous != null) {
            entryRemoved(false, key, previous, null);
        }
        return previous;//返回value
    }

sizeof:这个方法用于计算每个条目的大小,子类必须得复写这个类。

 protected int sizeOf(K key, V value) {//用于计算每个条目的大小
        return 1;
    }

snapshot方法,返回当前缓存中所有的条目集合

 public synchronized final Map<K, V> snapshot() {
        return new LinkedHashMap<K, V>(map);
    }

总结:

1.LruCache封装了LinkedHashMap,提供了LRU缓存的功能;

2.LruCache通过trimToSize方法自动删除最近最少访问的键值对;

3.LruCache不允许空键值;

4.LruCache线程安全;

5.继承LruCache时,必须要复写sizeof方法,用于计算每个条目的大小。

【源码】LruCache源码剖析

时间: 2024-11-10 07:52:29

【源码】LruCache源码剖析的相关文章

Spring Boot 揭秘与实战 源码分析 - 工作原理剖析

文章目录 1. EnableAutoConfiguration 帮助我们做了什么 2. 配置参数类 – FreeMarkerProperties 3. 自动配置类 – FreeMarkerAutoConfiguration4. 扩展阅读 3.1. 核心注解 3.2. 注入 Bean 结合<Spring Boot 揭秘与实战 源码分析 - 开箱即用,内藏玄机>一文,我们再来深入的理解 Spring Boot 的工作原理. 在<Spring Boot 揭秘与实战 源码分析 - 开箱即用,内藏

【MySQL源码】源码安装和启动mysql

--[MySQL源码]源码安装和启动mysql --------------------------------------2014/08/19 本机环境:ubuntu12.04,fedora-17 MYSQL版本:5.5.28 CMAKE版本:2.8.9 一.下载最新版本的cmake,解压后编译安装. sudo ./configure --prefix=/usr/local/etc/cmake-2.8.9 sudo make sudo make installsudo ln -s /usr/l

MINA2 源码学习--源码结构梳理

一.mina的整体框架结构及案例: 1.整体结构图: 简述:以上是一张来自网上比较经典的图,整体上揭示了mina的结构,其中IoService包含客户端IoConnector和服务端IoAcceptor两部分.即无论是客户端还是服务端都是这个结构.IoService封装了网络传输层(TCP和UDP),而IoFilterChain中mina自带的filter做了一些基本的操作之外,支持扩展.经过FilterChain之后最终调用IoHandler,IoHandler是具体实现业务逻辑的处理接口,具

Android二维码扫描源码

Android二维码扫描源码 支持平台:Android   运行环境:Eclipse   开发语言:Java 下载地址:http://t.cn/R7HfKOY 源码简介 源码运行截图                                                   

[Android 源码] Android源码下载

Android源码下载 为了能够顺利的下载Android的源码,同时也为了避免在网络上再次搜寻如何下载源码的麻烦,我把下载过程记录在这篇文档中. 官网中也有详细的介绍: http://source.android.com/source/downloading.html 1.环境设置问题 系统:Ubuntu 12.04 LTS 64bit 所需工具:curl, git-core, repo(repo的问题在后面有讲到)... 2.硬盘空间问题 首先,在下载源码之前,最首要的事请就是保证有足够的硬盘

ECO源码|GEC源码|挖矿源码|eco系统|gec程序

ECO源码|GEC源码|挖矿源码|eco系统|gec程序在最近一年的时间里,各种国产的资金盘疯狂的出现,其实看见有很多做的比较成功的,比如eco|gec都做得比较好 !在今年1月的时候,当时我也有冲动想尝试着做一个虚拟币的平台.与自己不是程序员所以对这个软件是一窍不通!但是就想要从网上买一个源码?在网上看了很多,一不小心就是两三万一个源码,也有几千块钱的源码!由于自己刚毕业所以就套了2000元在网上买了一个虚拟币的源码!在互站网里面买的,这是没过几天我在站长资源里面看见了一个十块钱的源码!当时抱

火币源APP现成源码

火币源APP现成源码,HBY火币源系统开发详情找梁经理(153微2202电6891)HBY火币源系统开发,HBY火币源模式平台开发,HBY火币源系统APP开发,HBY火币源软件开发,HBY火币源分红模式开发 ****************************非平台方,玩家勿扰**************************** 分享经济(Sharing Economy)是指将社会海量.分散.闲置资源.平台化.协同化地集聚.复用与供需匹配,从而实现经济与社会价值创新的新形态.分享经济强调

ZXing拍码后区分扫描到的是一维码、二维码、其他码

以前没有怎么接触过二维码,最近遇到一个问题,如何判断条码扫描到的是一维码还是二维码,经过自己艰苦奋斗一下午,加上网上查询资料, 总结出两种方式可以解决该问题(推荐采用第二种方式): 1.修改源码(具体后面会提到) 2.通过返回的编码来判断 实现方式一: 源码的修改,关键涉及到三个类,CaptureActivity.DecodeThread.DecodeFormatManager 1.首先让我们来看下Zxing的源码,里面有一个DecodeFormatManager编码管理类:该来原本的final

请求码和结果码

请求码和结果码 一.简介 请求码: 例如请求页面有多个button,根据请求码就知道是哪个button在请求 结果码: 多个请求可以打开多个页面,根据结果码就知道我们打开的是哪个界面 请求码是用来标识请求源的,结果码是用来标识结果源的. 二.具体步骤 这里演示结果码的 1.界面1里面的结果码是100 setResult(100, intent); 2.界面2里面的结果码是200 setResult(200, intent); 3.在主界面的 onActivityResult方法中根据结果码判断数