LruCache - Picasso的实现

所谓LRU,即 Least Recently Used,“最近使用”。
Picasso的LruCache实现了其自定义的一个interface:Cache,刚开始以为是extends关系,看了源码才发现不是。作为一个依赖Module,设计应当是让上层调用更容易扩展而无需影响底层实现的。细节值得学习。
和v4包里的LruCache一样,内部存储容器也是LinkedHashMap,双向链表结构。

final LinkedHashMap<String, Bitmap> map;

先看构造函数:

public LruCache(@NonNull Context context) {
   this(Utils.calculateMemoryCacheSize(context));
}

static int calculateMemoryCacheSize(Context context) {
   ActivityManager am = getService(context, ACTIVITY_SERVICE);
   boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0;
   int memoryClass = largeHeap ? am.getLargeMemoryClass() : am.getMemoryClass();
   // Target ~15% of the available heap.
   return (int) (1024L * 1024L * memoryClass / 7);
 }

先计算内存缓存的空间大小,看一下到ActivityManager层的调用:

static public int staticGetMemoryClass() {
  String vmHeapSize = SystemProperties.get("dalvik.vm.heapgrowthlimit", "");
  if (vmHeapSize != null && !"".equals(vmHeapSize)) {
    return Integer.parseInt(vmHeapSize.substring(0,   vmHeapSize.length()-1));
  }
  return staticGetLargeMemoryClass();
}

static public int staticGetLargeMemoryClass() {
  String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m");
  return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length() - 1));
}

不同设备的虚拟机为每个进程分配的内存是不一样的,可以在 /system/build.prop 中查看,例如这样的:

dalvik.vm.heapsize=24m
dalvik.vm.heapgrowthlimit=16m

当在manifest里指定android:largeHeap为true时,会申请获得最大的内存,即heapsize的大小24m,一般情况为false时为16m,Picasso里取1/7用为内存缓存的空间大小。
获得了内存缓存空间 size 后,又调用了以下构造方法:

public LruCache(int maxSize) {
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<>(0, 0.75f, true);
  }

LinkedHashMap的构造函数:

Parameters:
initialCapacity the initial capacity
loadFactor the load factor
accessOrder the ordering mode - true for access-order, false for insertion-order

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

如文档注释所说,第三个参数为true时,记录顺序为访问顺序(也就是这个特点才让它成为了LruCache的存储容器),为false时记录顺序为插入顺序。看源码时觉得第二个参数 “0.75” 很有意思,一般来说不是不提倡使用这种 “magic number” 吗?看了v4包里的LruCache的构造函数里也使用了这个0.75,跟进LinkedHashMap的源码看了一番,发现其默认值也是0.75,即使自定义值进去也没有在任何地方被赋值。其父类HashMap的文档里有这么一段说明:

The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
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.

这里涉及到一个HashMap的自动扩容问题,当哈希表中的条目数超出了load factor 与 capacity 的乘积时,通过调用 rehash 方法将容量翻倍。这个 load factor 就是用来在空间&时间上权衡时的一个最佳值。需知更多可参考:

构造函数之后,去看常用的方法 set(String key,Bitmap bitmap)

  @Override public void set(@NonNull String key, @NonNull Bitmap bitmap) {
    if (key == null || bitmap == null) {
      throw new NullPointerException("key == null || bitmap == null");
    }
    //计算要存入的bitmap的大小,如果直接超出了可分配的最大内存缓存空间就return
    int addedSize = Utils.getBitmapBytes(bitmap);
    if (addedSize > maxSize) {
      return;
    }
    //由于HashMap是线程不安全的,这里加个锁
    synchronized (this) {
      putCount++;
      size += addedSize;
      Bitmap previous = map.put(key, bitmap);
      if (previous != null) {
        //当key已经存过时,将map的容量减回刚才加前的值
        size -= Utils.getBitmapBytes(previous);
      }
    }

    trimToSize(maxSize);
  }

trimToSize(int maxSize)很关键:

 private void trimToSize(int maxSize) {
    while (true) {
      String key;
      Bitmap value;
      synchronized (this) {
        //1
        if (size < 0 || (map.isEmpty() && size != 0)) {
          throw new IllegalStateException(
              getClass().getName() + ".sizeOf() is reporting inconsistent results!");
        }
        //2
        if (size <= maxSize || map.isEmpty()) {
          break;
        }
        //3
        Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
        key = toEvict.getKey();
        value = toEvict.getValue();
        map.remove(key);
        size -= Utils.getBitmapBytes(value);
        evictionCount++;
      }
    }
  }

走到第3块代码时,就是说明当前的缓存容量已经超出可分配的最大内存了,迭代整个map,由于当前LinkedHashMap的存储顺序是按访问顺序存储,那么经由map.entrySet().iterator().next()迭代出的就是最先添加进且最不经常访问的那个Entry了,从map中将它remove(),然后更新此时map的容量,并且将移除次数的记录+1,直到符合第2块代码的判断,即缓存的内存容量小于最大可分配的空间。
get(String key)就更加简单:

 @Override public Bitmap get(@NonNull String key) {
    if (key == null) {
      throw new NullPointerException("key == null");
    }

    Bitmap mapValue;
    synchronized (this) {
      mapValue = map.get(key);
      if (mapValue != null) {
        hitCount++;
        return mapValue;
      }
      missCount++;
    }

    return null;
  }

命中了就 hitCount++ 记录并且返回该value,否则 missCount++ 。
在此类里发现一个clearKeyUri(String uri)方法,这是Picasso从2.5.0开始增加的一个方法,用于清除指定存储key的缓存的图片。看另外一个网友的文章,发现可以复写它实现一些其他自定义的清除功能,比如清除指定Activity或者Fragment的图片缓存,当然这样也需要在set时做一些处理。

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

推荐阅读更多精彩内容

  • 想给一位资深的HelloKitty 迷和粉色控的美女量身定做一款口金包,于是设计了这款独一无二的Hallo Kit...
    拼布童话阅读 352评论 0 0
  • 大概在年后,碾碎的爆竹纸零零散散落在地,混着才融不久的雪水,将水泥巷道贴得严丝密合。踩上去,就听到脚底清晰地发出“...
    酣河阅读 452评论 0 1
  • 你说,在这日新月异的现代生活中谈白头偕老,说相依相伴,太老土了。快节奏的生活,编奏的总是那激情瞬间的碰撞。 ...
    b825aefd4d93阅读 312评论 0 1
  • 一张车票,一提行李,一点期待,一份思念。 走在人山人海中,你回到了最熟悉的地方,与他人洋溢着笑容不同的是,你却面无...
    夜寂静阅读 148评论 0 0
  • 【日记摘录】虹桥机场“历险”记 人心之不同,如其面焉。——左丘明 《左传·襄公三十一年》 唯有人心相对时,咫尺之间...
    星光不问过路人s小懿阅读 195评论 0 0