Universal-Image-Loader从使用到源码分析

来源:互联网 发布:东欧现状知乎 编辑:程序博客网 时间:2024/06/06 02:59

为什么我要写一个已经过时框架的解析框架呢?

那在我自己的项目里面其实也不用这个框架了,一般用的是Glide框架,那为什么我要写这篇文章呢,是因为由于我目前水平有限,对于Glide的源码难以参透,那与其在一个高大上的框架上钻牛角尖,不如把已有的框架进行一个全面的解析,因为图片解析框架无非就是如何高效的加载图片,若参透了之前的框架,那么对新的框架新的技术,也就可以了解其本质,从而更游刃有余的去学习新的技术。

UIL的简单使用

首先导入jar包,在Application的onCreate方法中对UIL进行一些参数配置,如果不想配置,也可以直接使用默认的参数配置。
那么最常用的加载图片方法:

private void showImage() {        ImageLoader loader = ImageLoader.getInstance();        //最常用的方法//        loader.displayImage(url,iv);        //同样我们配置些参数        DisplayImageOptions options = new DisplayImageOptions.Builder()                //缓存到内存中                .cacheInMemory(true)                //缓存到磁盘中                .cacheOnDisk(true)                .build();        loader.displayImage(url,iv,options);    }

那么以上就是UIL的基本使用了。

内存缓存

我们一般去加载大量的图片的时候,都会做缓存策略,缓存又分为内存缓存和硬盘缓存。UIL默认使用的内存缓存是LruCache这个类,LRU是Least Recently Used 近期最少使用算法,我们可以给LruCache设定一个缓存图片的最大值,它会自动帮我们管理好缓存的图片总大小是否超过我们设定的值, 超过就删除近期最少使用的图片,而作为一个强大的图片加载框架,Universal-Image-Loader自然也提供了多种图片的缓存策略,下面就来详细的介绍下。

那么首先来分析一下LruCache干了些什么事

package com.nostra13.universalimageloader.cache.memory.impl;import android.graphics.Bitmap;import com.nostra13.universalimageloader.cache.memory.MemoryCacheAware;import java.util.Collection;import java.util.HashSet;import java.util.LinkedHashMap;import java.util.Map;/** * A cache that holds strong references to a limited number of Bitmaps. Each time a Bitmap is accessed, it is moved to * the head of a queue. When a Bitmap is added to a full cache, the Bitmap at the end of that queue is evicted and may * become eligible for garbage collection.<br /> * <br /> * <b>NOTE:</b> This cache uses only strong references for stored Bitmaps. * * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) * @since 1.8.1 */public class LruMemoryCache implements MemoryCacheAware<String, Bitmap> {    private final LinkedHashMap<String, Bitmap> map;    private final int maxSize;    /** Size of this cache in bytes */    private int size;    /** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */    public LruMemoryCache(int maxSize) {        if (maxSize <= 0) {            throw new IllegalArgumentException("maxSize <= 0");        }        this.maxSize = maxSize;        this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);    }    /**     * Returns the Bitmap for {@code key} if it exists in the cache. If a Bitmap was returned, it is moved to the head     * of the queue. This returns null if a Bitmap is not cached.     */    @Override    public final Bitmap get(String key) {        if (key == null) {            throw new NullPointerException("key == null");        }        synchronized (this) {            return map.get(key);        }    }    /** Caches {@code Bitmap} for {@code key}. The Bitmap is moved to the head of the queue. */    @Override    public final boolean put(String key, Bitmap value) {        if (key == null || value == null) {            throw new NullPointerException("key == null || value == null");        }        synchronized (this) {            size += sizeOf(key, value);            Bitmap previous = map.put(key, value);            if (previous != null) {                size -= sizeOf(key, previous);            }        }        trimToSize(maxSize);        return true;    }    /**     * Remove the eldest entries until the total of remaining entries is at or below the requested size.     *     * @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) {            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();                if (toEvict == null) {                    break;                }                key = toEvict.getKey();                value = toEvict.getValue();                map.remove(key);                size -= sizeOf(key, value);            }        }    }    /** Removes the entry for {@code key} if it exists. */    @Override    public final void remove(String key) {        if (key == null) {            throw new NullPointerException("key == null");        }        synchronized (this) {            Bitmap previous = map.remove(key);            if (previous != null) {                size -= sizeOf(key, previous);            }        }    }    @Override    public Collection<String> keys() {        synchronized (this) {            return new HashSet<String>(map.keySet());        }    }    @Override    public void clear() {        trimToSize(-1); // -1 will evict 0-sized elements    }    /**     * Returns the size {@code Bitmap} in bytes.     * <p/>     * An entry's size must not change while it is in the cache.     */    private int sizeOf(String key, Bitmap value) {        return value.getRowBytes() * value.getHeight();    }    @Override    public synchronized final String toString() {        return String.format("LruCache[maxSize=%d]", maxSize);    }}

我们可以看到在LruMemoryCache的初始化方法中初始化了一个LinkedHashMap和一个MaxSize,LinkedHashMap传入了true,是访问优先的模式,MaxSize代表了我们想要在内存中缓存所有图片的最大比特。还定义了一个size表示当前的所有图片大小。那么在put方法中,首先调用了一个sizeof()方法,sizeof()方法是用来计算put进去的图片大小,那么size会加上这个sizeof()的值,并且将bitmap放入map中,如果map已经缓存过了,就要减去之前计算得出的size。之后就会调用一个trimtosize(maxsize)方法,这个方法是用来调整map中图片的个数,如果size<=maxsize,那么什么都不做,如果反之,就删除map中的第一个元素,并循环这个过程,直到size<=maxsize。这就是整个LruMemoryCache的原理,并不复杂。
那么我们可以看到最为核心的类是LinkedHashMap,那LinkedHashMap是如何实现Lru算法的呢?

LinkedHashMap解析
LinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。
LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。
默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。可以重写removeEldestEntry方法返回true值指定插入元素时移除最老的元素。 让我们来看一下源码

/**  * 双向链表的表头元素。  */  private transient Entry<K,V> header;  /**  * LinkedHashMap的Entry元素。  * 继承HashMap的Entry元素,又保存了其上一个元素before和下一个元素after的引用。  */  private static class Entry<K,V> extends HashMap.Entry<K,V> {      Entry<K,V> before, after;      ……  }  // 通过源代码可以看出,在LinkedHashMap的构造方法中,实际调用了父类HashMap的相关构造方法来构造一个底层存放的table数组,并默认是插入顺序public LinkedHashMap(int initialCapacity, float loadFactor) {      super(initialCapacity, loadFactor);      accessOrder = false;  }  //LinkedHashMap重写了init()方法,来完成对header的初始化void init() {      header = new Entry<K,V>(-1, null, null, null);      header.before = header.after = header;  }  /* LinkedHashMap并未重写父类HashMap的put方法,而是重写了父类HashMap的put方法调用的子方法void recordAccess(HashMap m)   ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的双向链接列表的实现。*/public V put(K key, V value) {          if (key == null)              return putForNullKey(value);          int hash = hash(key.hashCode());          int i = indexFor(hash, table.length);          for (Entry<K,V> e = table[i]; e != null; e = e.next) {              Object k;              if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                  V oldValue = e.value;                  e.value = value;                  e.recordAccess(this);                  return oldValue;              }          }          modCount++;          addEntry(hash, key, value, i);          return null;  }//如果为访问优先,那么删除当前节点,添加到链表尾部void recordAccess(HashMap<K,V> m) {              LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;              if (lm.accessOrder) {                  lm.modCount++;                  remove();                  addBefore(lm.header);              }          }  void addEntry(int hash, K key, V value, int bucketIndex) {      // 调用create方法,将新元素以双向链表的的形式加入到映射中。      createEntry(hash, key, value, bucketIndex);      // 删除最近最少使用元素的策略定义      Entry<K,V> eldest = header.after;      //如果需要删除节点,那么就删除,如果不需要,则判断一下是否要扩容    if (removeEldestEntry(eldest)) {          removeEntryForKey(eldest.key);      } else {          if (size >= threshold)              resize(2 * table.length);      }  }  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;      // 调用元素的addBrefore方法,将元素加入到哈希、双向链接列表。      e.addBefore(header);      size++;  }  private void addBefore(Entry<K,V> existingEntry) {      after  = existingEntry;      before = existingEntry.before;      before.after = this;      after.before = this;  }  

那么总结一下put方法,如果map中已经有元素存在,并且是访问优先,就删除当前链表上的节点,并将当前节点添加到链表的尾部。
如果不存在,就直接添加到链表的尾部,并判断是否要移除最老的元素,如果需要移除,那么就直接移除,如果不需要移除也判断是否需要扩容。
LinkedHashMap重写了父类HashMap的get方法,实际在调用父类getEntry()方法取得查找的元素后,再判断当排序模式accessOrder为true时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。
OK,对LinkedHashMap是如何实现LruCache算法也有些了解。
那么现在了解了些基本原理,我们来梳理一下UIL的工作流程。

  • UI:请求数据,使用唯一的Key值索引Memory Cache中的Bitmap。
  • 内存缓存:缓存搜索,如果能找到Key值对应的Bitmap,则返回数据。否则执行第三步
  • 硬盘存储:使用唯一Key值对应的文件名,检索SDCard上的文件
  • 如果有对应文件,使用BitmapFactory.decode*方法,解码Bitmap并返回数据,同时将数据写入缓存。如果没有对应文件,执行第五步
  • 下载图片:启动异步线程,从数据源下载数据(Web)
  • 若下载成功,将数据同时写入硬盘和缓存,并将Bitmap显示在UI中
0 0