Picasso解析(2)-LruCache缓存分析

0.前言

上一次 Picasso解析(1)-一张图片是如何加载出来的中,我已经将整个Picasso如何解析一张网络图片的过程梳理了一遍。如果仅仅只是这样梳理一遍,那就只是为了看代码而看代码了,真正的我们还是应该学习到这些高质量代码中优美的设计与精湛的技巧。从这一篇开始,我就会逐一将我在当中学习到的地方分享出来。首先就从LruCache开始分析。

1.最近最少使用算法

LRU是Least Recently Used 最近最少使用算法,相信大学时候学过操作系统这门课的同学一定不会陌生。作为一种在内存当中存取数据的策略,LRU算法的本质是希望提高内存数据使用的命中率。核心思想就是:如果数据最近被访问过,那么将来被访问的几率也会更高。
具体一点举个例子,比如说我们有一个大小为3的内存块,现在我们有4,1,4,2,1,5,7,2八组数据要进入我们的内存块,那么我们来看一下内存块中的变化情况,在这里我们规定右边的数据是最近使用的数据
(1)4:4
(2)1:4,1
(3)4:1,4
(4)2:1,4,2
(5)1:4,2,1
(6)5:2,1,5
(7)7:1,5,7
(8)2:5,7,2
在这里我们可以很清晰地看出来,在LRU算法中,最近使用过的数据总是在队列的前面,总是淘汰处于队列中最末尾的数据。

2.自己动手实现LRUCache

在这里我用一个很简单很暴力的写法来演示一下LRU算法的原理:

public class LRUDemo {
    public static void main(String[] args) {
        LRU lru = new LRU(3);
        int[] arr = {4, 1, 4, 2, 1, 5, 7, 2};
        for (int i = 0; i < arr.length; i++) {
            lru.set(arr[i]);
            lru.traversal();
            System.out.println();
        }
    }
}

class LRU {
    private Queue<Integer> mLRUList;
    private int mMaxSize;
    private int mCurrentSize;

    public LRU(int maxSize) {
        mLRUList = new LinkedList<>();
        mMaxSize = maxSize;
        mCurrentSize = 0;
    }

    public void set(Integer data) {
        isExist(data);

        if (mCurrentSize >= mMaxSize) {
            mLRUList.poll();
        }

        mCurrentSize++;
        mLRUList.add(data);
    }

    public void isExist(int data) {
        if(mLRUList.remove(data)) {
            mCurrentSize--;
        }
    }

    public void traversal() {
        for (Integer i : mLRUList) {
            System.out.print(i + " ");
        }
    }
}

运行结果:


运行结果

在这里,我采用了LinkedList来模拟一个大小为3的队列,用了一种很暴力的方法,只要发现队列中存在重复的元素,就直接删除掉,然后将新的数据添加到队列的头部,这样就可以保证队列从头部到尾部依次是最近使用过的数据了。这里只是为了演示LRU的原理,这种写法当然是不提倡的。

3.Picasso中LRUCache的实现原理

前面主要针对LRU算法的一些原理进行了解释,接下来才是本文的核心部分,关于Picasso源码中的LRUCache实现原理的分析。

其实在android.util包中也有为我们提供LRUCache算法的实现,但是Picasso的作者还是自己实现了一个。我对比着看过了这两种LRUCache代码的实现,核心的实现思路都是一样。所以这里的分析以Picasso中的LRUCache为准。

(1).构造方法

  public LruCache(@NonNull Context context) {
    this(Utils.calculateMemoryCacheSize(context));
  }
  
  public LruCache(int maxSize) {
    if (maxSize <= 0) {
      throw new IllegalArgumentException("Max size must be positive.");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
  }

第一种构造方法是通过Utils方法里提供的计算内存方法来指定大小,其中在Picasso中需要的内存是当前应用内存的七分之一,也就是15%左右,具体为什么是七分之一我并不知道原因,个人认为应该是JakeWharton大神经过反复实践得出的最合理分配,所以我们也就默认这么大了。在第二个构造方法中,我们可以看到该方法初始化了一个LinkedMap成员变量用来存储图片。而这个LinkedMap的构造方法则与我们平常所使用的不太一样。我特意翻看了一下文档:


LinkedHashMap

第一个参数指定的是初始化容器大小。第二个参数是加载因子,这里传入的是0.75f。而第三个参数则是指定排序方法,如果为true,则是按照最常访问来进行排序,如果为false,则是按照插入时间来进行排序,在这里指定为true也很符合LRU算法的理念。所以我们重点来关注一下第二个参数,顺着构造方法进去阅读一下源码吧:

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }


public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY) {
            initialCapacity = MAXIMUM_CAPACITY;
        } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
            initialCapacity = DEFAULT_INITIAL_CAPACITY;
        }

        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // Android-Note: We always use the default load factor of 0.75f.

        // This might appear wrong but it's just awkward design. We always call
        // inflateTable() when table == EMPTY_TABLE. That method will take "threshold"
        // to mean "capacity" and then replace it with the real threshold (i.e, multiplied with
        // the load factor).
        threshold = initialCapacity;
        init();
    }

我们看到loadFactor参数居然除了简单判断了一下以外完全没有用到,google的程序员们非常善意的给我们留下了note,大概意思就是说我们总是使用默认值0.75。但还是令我感到一脸懵逼,于是我就到JAVA的官方文档里去看,发现了如下一段解释:

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

大概的意思是说,0.75这个值是权衡时间与空间利弊以后的一个最佳值,如果高于这个值,会节省一些空间,但会影响时间效率。我们再来看看Android源码中HashMap的一段注解:

    // Android-Note: We always use a load factor of 0.75 and ignore any explicitly
    // selected values.
    final float loadFactor = DEFAULT_LOAD_FACTOR;

google的大神们直接认准了0.75这个值,就算你传参传了个其他值也没用,所以难怪我们在看源码的时候没有看到任何的赋值操作,而只是做了一个简单的判断而已。当然,至于这个值为什么要这样设定,时间与空间上是如何平衡的,不在本文的讨论范围之内,等以后分析JAVA集合框架的时候在仔细研究。

(2).set方法

@Override
    public void set(@NonNull String key, @NonNull Bitmap bitmap) {
        if (key == null || bitmap == null) {
            throw new NullPointerException("key == null || bitmap == null");
        }

        int addedSize = Utils.getBitmapBytes(bitmap);
        if (addedSize > maxSize) {
            return;
        }

        synchronized (this) {
            putCount++;
            size += addedSize;
            Bitmap previous = map.put(key, bitmap);
            if (previous != null) {
                size -= Utils.getBitmapBytes(previous);
            }
        }

        trimToSize(maxSize);
    }

我们来看看核心的set方法。首先做了一些简单的判断,然后开始计算Bitmap的大小,如果说图片的大小直接就比我们整个缓存的大小都大的话,那就直接return掉了,如果不是的话,就进入一段同步代码块(保证多线程加载图片情况下的线程安全)。在这段同步代码块里,首先会将bitmap put进LinkedHashmap中,当Map中存在同样key的value时,put方法就会返回给旧的值,同时也会根据添加和替换的结果来计算当前内存中的size。同步代码块的代码执行完毕后会做一个trimToSize操作:


private void trimToSize(int maxSize) {
        while (true) {
            String key;
            Bitmap 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<String, Bitmap> toEvict = map.entrySet().iterator()
                        .next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= Utils.getBitmapBytes(value);
                evictionCount++;
            }
        }
    }

一上来我们就发现这是一个无限循环,该方法主要就是根据当前的size大小来进行一个判断。如果当前的size没有超过maxSize的话,循环就会中指,如果size大于maxSize的话,就会删除掉最久没有使用的图片,并一直到size最终小于maxSize时终止。那么,在这里为什么我们通map.entrySet().iterator().next()方法就能拿到最久没有使用的对象呢?这就涉及到之前构造方法里传参的第三个参数accessOrder,当它为true的时候,就是按照访问的顺序来进行排序,所以我们才能顺利地拿到这个需要删除的对象,其中具体的原理也是等到以后分析JAVA集合框架的时候再说吧。

4.总结

至此LRUCache缓存类的原理就分析完毕了,核心的思想便是一个最近最少使用算法。我们可以看到JakeWharton大神巧妙的利用到了LinkedHashmap中的一些高级特性来实现了一个LRU算法,为我们以后设计诸如此类算法策略时提供了一些新的思路。

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

推荐阅读更多精彩内容