说说Android LRU缓存算法实现学习笔记(一)

在我们的手机应用开发时候,我们经常会遇到大数据访问的时候,我们通常会考虑以下几个方面的情况。一、手机内存的限制还必须保证应用反应的流畅;二、尽量小的流量消耗,不然,你的应用流畅度再好体验再好,用户还是会毫不犹豫的卸载掉你的应用。大数据量访问的情况下,数据缓存是我们一定会考虑到的解决方案。而作为缓存,我们很重要的会考虑以下几点:1.访问速度;2.逐出旧的缓存策略;3.最好还能考虑到一定的并发度。这篇我们主要说说LRU策略的缓存算法实现,我们就用图片缓存为例来谈谈Android应用开发中的缓存实现。

首先我们来看看谷歌官方的推荐的缓存:在Android3.0加入的LruCache和 DiskLruCache(硬盘缓存结构)类。我们从代码的实现知道,LruCache和DiskLruCache缓存的实现都是基于JDK的LinkedHashMap集合来实现。下面我们来从LinkedHashMap的源码的分析来开始学习。

通过源码我们知道,LinkedHashMap是继承HashMap,底层结构不仅使用HashMap来保存元素,同时通过继承HashMapEntry 实现双向链表的结构来关联其他的元素。我们先来看LInkedHashMap的节点实现:

    /**
     * LinkedHashMap entry.
     */
    private static class Entry<K,V> extends HashMap.Entry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;

LinkedHashMap的节点Entry继承自HashMap.Entry来实现,增加两个引用来分别指向前一个元素和后一个元素。LinkedHashMap的实现是在HashMap的基础上增加双链表的结构。

我们再来看看LinkedHashMap的初始化,我们看到构造函数参数最多的情况:

/**
     * 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指定排序,默认为false,为fasle的时候,插入顺序排序,为true时候,访问顺序排序
    }

我们看到重写的基类的初始化方法init实习:

/**
     * Called by superclass constructors and pseudoconstructors (clone,
     * readObject) before any entries are inserted into the map.  Initializes
     * the chain.
     */
    void init() {
        header = new Entry<K,V>(-1, null, null, null);
        header.before = header.after = header;
    }

此处初始化头指针给循环指定。下面我们重点来看看集合最总要的两个方法put和get的实现。我们先看put方法,LinkedHashMap的put方法并没有重写HashMap的put方法。只是重写了put方法下的addEntry方法,addEntry方法执行时候是当LinkedHashMap插入新的结点的时候执行。

/**
     * This override alters behavior of superclass put method. It causes newly
     * allocated entry to get inserted at the end of the linked list and
     * removes the eldest entry if appropriate.
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        createEntry(hash, key, value, bucketIndex);

        // Remove eldest entry if instructed, else grow capacity if appropriate
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        } else {
            if (size >= threshold)
                resize(2 * table.length);
        }
    }

    /**
     * This override differs from addEntry in that it doesn't resize the
     * table or remove the eldest entry.
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry<K,V> old = table[bucketIndex];
	Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);
        size++;
    }

我们看我们产生新结点的方法creatEntry实现,我们先给找到哈希表上对应的位置,然后给新的Entry指定前后的节点。执行方法e.addBefore(header)代码如下:

 /**
         * Inserts this entry before the specified existing entry in the list.
         */
        private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

我们看到代码实现的功能就是把新结点添加到header结点前。我们再回到addEntry的代码实现,我们看 Entry<K,V> eldest = header.after;if (removeEldestEntry(eldest))的执行,我们把header后结点理解成我们最旧的节点,removeEldestEntry(eldest)方法的实现:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

removeEldestEntry方法执行默认返回false,意思就是我们LinkedHashMap默认不会逐去旧的节点。我们可以通过在子类重载来重写removeEldestEntry的实现,来修改LRU策略。下面我们用图示来表示put方法的执行一下代码的执行过程:

	   map.put("22", "xxx");

首先map执行前的结构是这样:

为了不让图上密密麻麻的都是引用箭头乱窜,我省略了HashMap的很多Entry结点的before和after的指向,我们只是重点表示header的引用指向。我们执行方法map.put("22","xxx")后的数据结构变成这样:

我们把key为22的节点通过hash算法,指定到eee的后面,同时我们把header的before指向修改为指向key为22的节点。分析完我们的put方法,我们接着来分析get方法的实现,代码如下:

public V get(Object key) {
        Entry<K,V> e = (Entry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }

我们看到get方法很简单,首先我们去Map取key对应的value是否存在,如果不存在,我们返回null。如果存在,我们执行e.recoedAccess(this)方法。

 /**
         * This method is invoked by the superclass whenever the value
         * of a pre-existing entry is read by Map.get or modified by Map.set.
         * If the enclosing Map is access-ordered, it moves the entry
         * to the end of the list; otherwise, it does nothing.
         */
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) { //此处的判断LinkedHashMap的排序顺序
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

recodeAccess方法中,当我们的排序顺序为false,为按插入排序顺序的时候,我们直接退出方法。当我们的排序顺序为访问排序顺序的时候,我们执行remove方法,把结点从链表里切除出来,然后执行addBefore方法时候,我们把结点插入到header结点的前面。这样,我们就实习每次根据访问位置来排序的操作。

下面我们通过图的例子来说明,当LinkedHashMap的排序为访问排序的时候,我们get操作的过程,LinkedHashMap的数据结构的变化。我们执行的代码如下:

put.get("15");//我们假设key为15对应的value为"bbb"

我们在执行完map.put("22","xxx")后,再执行put.get("15")的变化如下:

以上为执行get方法之后的代码发生变化,"bbb”对应的节点其实没有发生变化,只是在链表结构中的指针发生了变化,因此对应的迭代器访问的时候,我们读取位置不一样。

因为我们可以知道我们LRU的节点肯定是在header的after指向节点。当我们重写removeEldestEntry来修改LRU策略的时候,我们也是首先逐出header的after所指向的节点。然后,我们最后来看LinkedHashmap的迭代器的实现如下:

private abstract class LinkedHashIterator<T> implements Iterator<T> {
        Entry<K,V> nextEntry    = header.after;
        Entry<K,V> lastReturned = null;

        /**
         * The modCount value that the iterator believes that the backing
         * List should have.  If this expectation is violated, the iterator
         * has detected concurrent modification.
         */
        int expectedModCount = modCount;

        public boolean hasNext() {
            return nextEntry != header;
        }

        public void remove() {
            if (lastReturned == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

            LinkedHashMap.this.remove(lastReturned.key);
            lastReturned = null;
            expectedModCount = modCount;
        }

        Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (nextEntry == header)
                throw new NoSuchElementException();

            Entry<K,V> e = lastReturned = nextEntry;
            nextEntry = e.after; //此处我们可知我们的迭代器实现访问节点的after引用指向的节点。
            return e;
        }
    }

从LinkedHashMap实现的实现的迭代器内部类LinkedHashIterator看,我们迭代器的元素访问顺序是从header节点开始往after方向开始迭代。以上为对LinkedHashMap的分析简单分析,下一节我们会针对LinkedHashMap的数据结构的特性来说说,用LinkedHashMap结构实现缓冲的快速访问的优势,还会结合常用的开源实现,来说说实现一定并发的Android下缓存实现。

时间: 2024-12-19 19:08:28

说说Android LRU缓存算法实现学习笔记(一)的相关文章

说说Android LRU缓存算法实现笔记(二)--LRU的应用

上一篇文章说说Android LRU缓存算法实现学习笔记(一) 中我们介绍了最常用的实现LRU缓存的数据结构LinkedHashMap,这一节我们会针对LinkedHashMap的数据结构的特性,来自己实现缓存结构和学习Android源码和项目中对缓存的完善. 上一篇说到对于缓存实现,我们很重要的会考虑以下几点:1.访问速度:2.逐出旧的缓存策略:3.最好还能考虑到一定的并发度.LinkedHashMap对哈希表的实现保证了我们缓存的快速访问速度,我们通过源码知道,LinkedHashMap默认

Android开源项目SlidingMenu本学习笔记(两)

我们已经出台SlidingMenu使用:Android开源项目SlidingMenu本学习笔记(一个),接下来再深入学习下.依据滑出项的Menu切换到相应的页面 文件夹结构: watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZGVuZzB6aGFvdGFp/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" > 点击Bluetooth能够切换到对应的

数据结构与算法基础学习笔记

*********************************************            ---算法与数据机结构--- 数据结构:由于计算机技术的发展,需要处理的对象不再是纯粹的数值,还有像字符,表,图像等具有一定结构的数据,需要用好的算法来处理这些数据. 我们把现实中大量而又复杂的问题以特定的数据类型的特定的存储结构保存到主存储器中,以及在此基础上为实现某个功能而执行的相应操作(查找排序),这个相应的操作也叫算法. 数据结构 = 个体 +个体的关系算法 =对存储数据的操

【算法导论学习笔记】第3章:函数的增长

????原创博客,转载请注明:http://www.cnblogs.com/wuwenyan/ ? 当算法的输入n非常大的时候,对于算法复杂度的分析就显得尤为重要,虽然有时我们能通过一定的方法得到较为精确的运行时间,但是很多时候,或者说绝大多数时候,我们并不值得去花精力求得多余的精度,因为精确运行时间中的倍增常量和低阶项已经被输入规模本身的影响所支配.我们需要关心的是输入规模无限增加,在极限中,运行时间是如何随着输入规模增大而增加的,通常来说,在极限情况下渐进地更优的算法在除很小的输入外的所有情

Android开源项目SlidingMenu的学习笔记(二)

在前面已经介绍了SlidingMenu的用法:Android开源项目SlidingMenu的学习笔记(一),接下来再深入学习下,根据滑出项的Menu切换到对应的页面 目录结构: 点击Bluetooth可以切换到相应的界面 关键代码 MainActivity.java package com.dzt.slidingmenudemo; import android.app.Fragment; import android.app.FragmentManager; import android.app

Android Activity和Intent机制学习笔记

Activity Android中,Activity是所有程序的根本,所有程序的流程都运行在Activity之中,Activity具有自己的生命周期(见http://www.cnblogs.com/feisky/archive/2010/01/01/1637427.html,由系统控制生命周期,程序无法改变,但可以用onSaveInstanceState保存其状态). 对于Activity,关键是其生命周期的把握(如下图),其次就是状态的保存和恢复(onSaveInstanceState onR

面试挂在了 LRU 缓存算法设计上

好吧,有人可能觉得我标题党了,但我想告诉你们的是,前阵子面试确实挂在了 RLU 缓存算法的设计上了.当时做题的时候,自己想的太多了,感觉设计一个 LRU(Least recently used) 缓存算法,不会这么简单啊,于是理解错了题意(我也是服了,还能理解成这样,,,,),自己一波操作写了好多代码,后来卡住了,再去仔细看题,发现自己应该是理解错了,就是这么简单,设计一个 LRU 缓存算法. 不过这时时间就很紧了,按道理如果你真的对这个算法很熟,十分钟就能写出来了,但是,自己虽然理解 LRU

【Android适配器系列】BaseAdapter学习笔记

慕客网-Android必学-BaseAdapter的使用与优化-学习笔记 什么是数据适配器 数据适配器是数据源与视图(View)之间的桥梁,建立了两者之间的适配关系.数据的来源是各种各样的,但View能显示的格式却是有一定要求的,数据适配器是把各种各样的数据源转化成为View能显示的数据格式. 优点: 将数据的来源与数据的显示进行了解耦,降低程序的耦合性,提高可扩展性. BaseAdapter是Android各种各样适配器里最通用的适配器. BaseAdapter基本结构 public int

算法导论学习笔记 -(1)

一个ACM若菜,趁着ACM淡季,开始学习算法导论了,经过一年的ACM学习,逐渐的发现,学东西,深入才是王道,以前学习一个算法,总是看懂了就开始做题,到后来才发现很多题目,会算法,却不知道是用这个算法,这就是算法理解的不到位的后果,从今天开始,定下目标: 1.深入系统的学习算法, 2.学会纸上写伪代码,每章的代码自己先在纸上写一遍, 3.每节的学习笔记和算法都要写在博客上. 在博客上记录自己的学习笔记,方便以后温习.   欢迎各路大神路过指正错误之处. 现在,先写写书上的第一个算法,插入排序. 算