Android 源码之LruCache

“三级缓存”这个词我想搞Android 都知道,与其相关的就是LruCache,今天我们就来说说LruCache。
LruCache(Least Recently Used),即最近最少使用算法。
在android源码包名为android.util下就有这么一个类:LruCache,已经帮我们实现好了LruCache算法。

在说LruCache之前,总少不要说LinkedHashMap,而LinkedHashMap是继承于HashMap的,而HashMap相关东西可参考笔者之前写的:HashMap、ArrayMap、SparseArray

我们还是直接上demo:

        System.out.println("————————LinkedList——————————");
        LinkedList<String> linkedList=new LinkedList<>();
        linkedList.add("1");
        linkedList.add("2");
        linkedList.add("3");
        linkedList.add("4");
        System.out.println("获取第二个数据:"+linkedList.get(1));
        for(String string:linkedList){
            System.out.println("数据:"+string);
        }
        System.out.println("—————————HashMap—————————");
        HashMap<Integer,String> hashMap=new HashMap<>();
        hashMap.put(10,"1");
        hashMap.put(2,"2");
        hashMap.put(3,"3");
        hashMap.put(4,"4");
        System.out.println("获取key为2的数据:"+hashMap.get(2));
        for (Map.Entry<Integer,String> entry:hashMap.entrySet()){
            System.out.println("数据:"+entry.getValue());
        }

打印出来的结果如下:

————————LinkedList——————————
获取第二个数据:2
数据:1
数据:2
数据:3
数据:4
—————————HashMap—————————
获取key为2的数据:2
数据:2
数据:3
数据:4
数据:1

很显然LinkedList是有序的,而HashMap是无序的,所以,我们今天要说的LinkedHashMap就应运而生了,我们继续看demo:

       System.out.println("—————LinkedHashMap(无参)——————");
        LinkedHashMap<Integer,String> linkedHashMap0=new LinkedHashMap<>();
        linkedHashMap0.put(10,"1");
        linkedHashMap0.put(2,"2");
        linkedHashMap0.put(3,"3");
        linkedHashMap0.put(4,"4");
        System.out.println("获取key为2的数据:"+linkedHashMap0.get(2));
        for (Map.Entry<Integer,String> entry:linkedHashMap0.entrySet()){
            System.out.println("数据:"+entry.getValue());
        }
        System.out.println("—————LinkedHashMap(有参)——————");
        LinkedHashMap<Integer,String> linkedHashMap=new LinkedHashMap<>(0, 0.75f, true);
        linkedHashMap.put(10,"1");
        linkedHashMap.put(2,"2");
        linkedHashMap.put(3,"3");
        linkedHashMap.put(4,"4");
        System.out.println("获取key为2的数据:"+linkedHashMap.get(2));
        for (Map.Entry<Integer,String> entry:linkedHashMap.entrySet()){
            System.out.println("数据:"+entry.getValue());     
        }

打印出来的结果如下:

—————LinkedHashMap(无参)——————
获取key为2的数据:2
数据:1
数据:2
数据:3
数据:4
—————LinkedHashMap(有参)——————
获取key为2的数据:2
数据:1
数据:3
数据:4
数据:2

第一个LinkedHashMap无参实现了有序的Map
第二个LinkedHashMap有参(第三个参数accessOrder为true时)不止实现了有序的Map,而且将最近一次调用get获取的数据置于队尾。

好啦,LinkedHashMap基本的东西也就这样子了,接下来我们来看看LruCache的源码,首先在这个类最开始部分的注释已经给了使用示例,如下图:

LruCache使用示例

用法非常简单,接下来我们看看其构造函数:

   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,而且就是上面LinkedHashMap有参的形式,就是利用其来实现最近最少使用算法的。

LruCache的源码非常简单,没什么好说的,但是里面有一个方法却让人有点摸不着头脑:

    /**
     * @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 CHANGE

                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

这个方法是用来当存于LinkedHashMap中数据大小超过最大限制数时,将最近最少使用的数据进行移除的,按照LinkedHashMap特性,最近使用到会至于队尾,然而我们摘取上面关键的代码:

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

这样找到的不就是最后一个元素吗?如果按照这代码执行的话,被移除的就是最后一个元素,也就是最近使用过的元素,这根本就违背了“最近最少使用”原则嘛!

这到底是怎么回事?

我越看越不对劲,这代码这么简单我没理由会理解错的啊!难不成我对“最近最少使用算法”本来就存在误解?吓得我赶紧百度一番,没错啊!
于是,我写了个demo来验证一下:

        LruCache<String,Integer> lruCache=new LruCache<>(4);
        lruCache.put("1",1);
        lruCache.put("2",2);
        lruCache.put("3",3);
        lruCache.put("4",4);
        lruCache.get("2");
        Map<String, Integer> snapshot = lruCache.snapshot();
        for(Map.Entry<String,Integer> entry:snapshot.entrySet()){
            LogUtil.loge("数据:"+entry.getValue());
        }
        LogUtil.loge("------------------超出后------------------");
        lruCache.put("5",5);
        snapshot = lruCache.snapshot();
        for(Map.Entry<String,Integer> entry:snapshot.entrySet()){
            LogUtil.loge("数据:"+entry.getValue());
        }

打印出来的结果如下:

数据:1
数据:3
数据:4
数据:2
------------------超出后------------------
数据:3
数据:4
数据:2
数据:5

幸好结果正常,还有没颠覆我对LruCache算法的理解O(∩_∩)O
那到底是为什么呢?干脆点,打断点进去看看啊,然而我惊奇地发现,上面我有疑问的代码变成了这样子的:

    Map.Entry<K, V> toEvict = map.eldest();
     if (toEvict == null) {
          break;
    }

如果是这样子的话倒是没错,因为在LinkedHashMap中有这么一个方法,返回的就是对头元素,这样确实符合LruCache算法,当超出时最开始加入的会被移除。

    // Android-added: eldest(), for internal use in LRU caches
    /**
     * Returns the eldest entry in the map, or {@code null} if the map is empty.
     * @hide
     */
    public Map.Entry<K, V> eldest() {
        return head;
    }

其实,你用断点进去发现代码不一样时,你会发现原来是android版本的问题,比如笔者用的夜神模拟器版本为19,其源码就是上面那样子的。而一开始我查看源码时用的是项目里配的目标版本为28的源码,这就是区别!

于是,我又打开了AndroidStudio自带的API为28的模拟器,我想继续进断点看看,结果呢?诡异的事又发生了,怎么感觉错行了呢?感觉执行的和我看到的不是同一行呢?而且也没有运行上面那个诡异代码的循环,好像到附近也是一行跳过,倒跟API 19的那个有点像了····

最后,我终于注意到那诡异源码上的那些英文注释:

// 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.

然后我一口气查看最近几个版本的源码,发现原来在API 22的时候LruCache类中就出现了这样子的注释和诡异代码,而API 23-27又恢复正常,现在API 28又变成这样子诡异了,简直了······

其实嘛,从上面的英文注释我们大概也能猜到是为了兼容性吧,算了,这里就不作深究了,毕竟注释也说清楚了,那代码是无效的,我想这也是断点进去会诡异的原因吧。
我们来说另外一个吧,LinkedHashMap如何获取头部元素和尾部元素呢?
最简单的做法:

        Map.Entry<Integer,String> first=null;
        Map.Entry<Integer,String> last=null;
        for (Map.Entry<Integer,String> entry:linkedHashMap.entrySet()){  
            if(first==null){
                first=entry;
                //break;
            } 
            last=entry;
        }

这样写当然没毛病,就是在获取尾部元素时效率有点低。
我们注意到上面LruCache中正常代码有用到LinkedHashMap的一个方法:eldest(),根据注释,我们知道这个方法是android为LruCache加的,而且使用了@hide注解,所以,我们是无法直接访问到的,嘿嘿,这话的意思是,我们可以间接访问到,必须滴,反射大法上!

       try {
            Field headField = linkedHashMap.getClass().getDeclaredField("head");
            Field tailField = linkedHashMap.getClass().getDeclaredField("tail");
            headField.setAccessible(true);
            tailField.setAccessible(true);
            Map.Entry<Integer,String> head = (Map.Entry<Integer, String>) headField.get(linkedHashMap);
            Map.Entry<Integer,String> tail = (Map.Entry<Integer, String>) tailField.get(linkedHashMap);
            if(head!=null){
                System.out.println("头部元素:"+head.getValue());
            }
            if(tail!=null){
                System.out.println("尾部元素:"+tail.getValue());
            }

        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }

代码也非常简单,就不多说了!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容