universal-image-loader的缓存,带你从源码分析

1.在universal-image-loader中的内存缓存策略

我们先来了解下强引用和弱引用的概念:

强引用(StrongReference):从字面上的意思就是我对你强制引用,比如说我们经常写的new 一个对象,即使程序抛出 outOfMemory 错误也不会被垃圾回收器回收;

弱引用(WeakReference):它与软引用的区别在于,一旦垃圾回收器扫描到weakReference对象,不管内存是否够用都会被回收,然后释放内存。

OK,了解完概念后,我们看 universal-image-loader 源码中提供的缓存策略,先来看下源码中的结构图:

1.png

有三种内存缓存策略,分别是:

  • 只使用的是强引用缓存
    LruMemoryCache:开源框架默认的内存缓存类,缓存的是Bitmap的强引用;

  • 使用强引用和弱引用的结合,相关的类有:
    FIFOLimitedMemoryCache : 先进先出的缓存策略,当超过设定值后,把最先添加进去的bitmap移出去;
    LRULimitedMemoryCache : 内部使用lru算法,与LruMemoryCache不同的是,它使用的是Bitmap的弱引用;
    LargestLimitedMemoryCache : 如果缓存大小超过设定值,删除最大的bitmap图片;
    LimitedAgeMemoryCache : 如果缓存里的图片存在的时间超出我们设置的最大时间,就被移除出去;
    UsingFreqLimitedMemoryCache : 如果缓存的图片超出设置的总量,就会删除使用频率少的Bitmap;

  • 只使用弱引用的缓存
    WeakMemoryCache : 缓存bitmap的空间不受限制,但是不稳定,容易被系统回收掉。


一般我们都是使用默认的强引用缓存策略,即LruMemoryCache,当然通过ImageLoaderConfigration创建时也可以设置别的缓存策略:
通过 .memoryCache()可以设置

    ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(
                context)
                // 线程池内加载的数量
                .threadPoolSize(4).threadPriority(Thread.NORM_PRIORITY - 2)
                .memoryCache(new WeakMemoryCache())
                .denyCacheImageMultipleSizesInMemory()
                .discCacheFileNameGenerator(new Md5FileNameGenerator())
                // 将保存的时候的URI名称用MD5 加密
                .tasksProcessingOrder(QueueProcessingType.LIFO)
                // .defaultDisplayImageOptions(DisplayImageOptions.createSimple())
                // .writeDebugLogs() // Remove for release app
                .build();
        // Initialize ImageLoader with configuration.
        ImageLoader.getInstance().init(config);// 全局初始化此配置

接下来我们重点看下LruMemoryCache内存缓存策略,看下它究竟是怎么实现在空间有限的情况下,保留最近使用的图片。首先贴出LruMemory的源码:

package com.nostra13.universalimageloader.cache.memory.impl;

import android.graphics.Bitmap;
import com.nostra13.universalimageloader.cache.memory.MemoryCache;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import java.util.Set;

public class LruMemoryCache
  implements MemoryCache
{
  private final LinkedHashMap<String, Bitmap> map;
  private final int maxSize;
  private int size;

  public LruMemoryCache(int maxSize)
  {
    if (maxSize <= 0) {
      throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap(0, 0.75F, true);
  }

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

    synchronized (this) {
      return (Bitmap)this.map.get(key);
    }
  }

  public final boolean put(String key, Bitmap value)
  {
    if ((key == null) || (value == null)) {
      throw new NullPointerException("key == null || value == null");
    }

    synchronized (this) {
      this.size += sizeOf(key, value);
      Bitmap previous = (Bitmap)this.map.put(key, value);
      if (previous != null) {
        this.size -= sizeOf(key, previous);
      }
    }

    trimToSize(this.maxSize);
    return true;
  }

  private void trimToSize(int maxSize)
  {
    while (true)
      synchronized (this) {
        if ((this.size < 0) || ((this.map.isEmpty()) && (this.size != 0))) {
          throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
        }

        if ((this.size > maxSize) && (this.map.isEmpty()))
        {
          break;
        }
        Map.Entry toEvict = (Map.Entry)this.map.entrySet().iterator().next();
        if (toEvict == null) {
          break;
        }
        String key = (String)toEvict.getKey();
        Bitmap value = (Bitmap)toEvict.getValue();
        this.map.remove(key);
        this.size -= sizeOf(key, value);
      }
  }

  public final Bitmap remove(String key)
  {
    if (key == null) {
      throw new NullPointerException("key == null");
    }

    synchronized (this) {
      Bitmap previous = (Bitmap)this.map.remove(key);
      if (previous != null) {
        this.size -= sizeOf(key, previous);
      }
      return previous;
    }
  }

  public Collection<String> keys()
  {
    synchronized (this) {
      return new HashSet(this.map.keySet());
    }
  }

  public void clear()
  {
    trimToSize(-1);
  }

  private int sizeOf(String key, Bitmap value)
  {
    return value.getRowBytes() * value.getHeight();
  }

  public final synchronized String toString()
  {
    return String.format("LruCache[maxSize=%d]", new Object[] { Integer.valueOf(this.maxSize) });
  }
}

我们只看重点,首先我们看它的get方法:

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

   synchronized (this) {
     return (Bitmap)this.map.get(key);
   }
 }

通过key取Bitmap,乍一看挺简单的哈,也没有做很复杂的判断,我们看到它是从一个map里取entry,然后我们查看这个map的get方法:

/**
     * Returns the value of the mapping with the specified key.
     *
     * @param key
     *            the key.
     * @return the value of the mapping with the specified key, or {@code null}
     *         if no mapping for the specified key is found.
     */
    @Override public V get(Object key) {
        /*
         * This method is overridden to eliminate the need for a polymorphic
         * invocation in superclass at the expense of code duplication.
         */
        if (key == null) {
            HashMapEntry<K, V> e = entryForNullKey;
            if (e == null)
                return null;
            if (accessOrder)
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                if (accessOrder)
                    makeTail((LinkedEntry<K, V>) e);
                return e.value;
            }
        }
        return null;
    }

这个方法除了返回key对应的value外,我们注意到一个accessOrder变量控制的变量,里面有一个makeTail方法,我们进去看看这个makeTail方法:

/**
     * Relinks the given entry to the tail of the list. Under access ordering,
     * this method is invoked whenever the value of a  pre-existing entry is
     * read by Map.get or modified by Map.put.
     */
    private void makeTail(LinkedEntry<K, V> e) {
        // Unlink e
        e.prv.nxt = e.nxt;
        e.nxt.prv = e.prv;

        // Relink e as tail
        LinkedEntry<K, V> header = this.header;
        LinkedEntry<K, V> oldTail = header.prv;
        e.nxt = header;
        e.prv = oldTail;
        oldTail.nxt = header.prv = e;
        modCount++;
    }

Relinks the given entry to the tail of the list 就是说会对当前key对应的entry的顺序移动到集合的最后,但前提是这个accessOrder必须是true,我们看下这个变量在哪里赋值的:

/**
     * Constructs a new {@code LinkedHashMap} instance with the specified
     * capacity, load factor and a flag specifying the ordering behavior.
     *
     * @param initialCapacity
     *            the initial capacity of this hash map.
     * @param loadFactor
     *            the initial load factor.
     * @param accessOrder
     *            {@code true} if the ordering should be done based on the last
     *            access (from least-recently accessed to most-recently
     *            accessed), and {@code false} if the ordering should be the
     *            order in which the entries were inserted.
     * @throws IllegalArgumentException
     *             when the capacity is less than zero or the load factor is
     *             less or equal to zero.
     */
    public LinkedHashMap(
            int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        init();
        this.accessOrder = accessOrder;
    }

在这个LinkedHashMap初始化的时候赋值的,OK,我们再看下LruMemoryCache里的LinkedHashMap的初始化:

this.map = new LinkedHashMap(0, 0.75F, true);

看到了吧,源码里传入true,也就是说我们get的时候总会把调整当前key对应的entry,调整到链表的最后,the tail of the list。

接下来,我们来看下LruMemoryCache put方法:

public final boolean put(String key, Bitmap value)
  {
    if ((key == null) || (value == null)) {
      throw new NullPointerException("key == null || value == null");
    }

    synchronized (this) {
      this.size += sizeOf(key, value);
      Bitmap previous = (Bitmap)this.map.put(key, value);
      if (previous != null) {
        this.size -= sizeOf(key, previous);
      }
    }

    trimToSize(this.maxSize);
    return true;
  }

又是好简短,首先一个sizeOf方法,根据图片key,value,计算图片的大小,然后累积赋值 this.size ,之后会调用trimToSize方法:

 private void trimToSize(int maxSize)
  {
    while (true)
      synchronized (this) {
        if ((this.size < 0) || ((this.map.isEmpty()) && (this.size != 0))) {
          throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
        }

        if ((this.size > maxSize) && (this.map.isEmpty()))
        {
          break;
        }
        Map.Entry toEvict = (Map.Entry)this.map.entrySet().iterator().next();
        if (toEvict == null) {
          break;
        }
        String key = (String)toEvict.getKey();
        Bitmap value = (Bitmap)toEvict.getValue();
        this.map.remove(key);
        this.size -= sizeOf(key, value);
      }
  }

循环遍历,当添加的图片大于最大值时,就会remove掉,这时候我们会思考会不会误删掉最近使用的Bitmap呢?答案是不会的,为什么呢,LruMemoryCache的最近使用指的是最近操作get put方式操作的bitmap缓存,刚才我们已经说了,当我们get的时候会把这个entry的顺序调到链表的尾部,当我们remove时,从链表头部开始遍历删除bitmap,直到size小于等于我们设置的最大值,自然不会误删掉我们最近使用的Bitmap了。借用一下别人的一个图,非常形象:

2.png

OK,这样看下来,我们通过查看LruMemoryCache的get put remove方法,了解了它的缓存策略。再来就是它的磁盘缓存。

我们先看下结构图:

3.png

思考就是我们如何保证下载到本地的图片不会重复或者覆盖掉呢?那就是保证命名唯一,我们可以看到有一个name的包,就是用来命名的,俗称命名生成器。

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

推荐阅读更多精彩内容