分析Universal-Image-Loader的缓存原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ezview_uniview/article/details/80820205

        

        最近阅读了一下UIL的源码,因为在Android开发中,用第三方框架加载图片是很正常的,但是原理一定要自己有所了解,通过github下载源码,看下使用文档,集成到工程中,是最基本的技能。了解框架后面的原理,才是我们真正要学习的东西。

        首先说下Android 中的缓存,一般所说的三级缓存分为内存缓存、磁盘缓存、网络缓存。  网络缓存实际上还是下载后,缓存到本地,本质上就是两级缓存,内存缓存和磁盘缓存。

        UIL加载图片的流程是先从内存中看bitmap是否存在,如果内存中不存在,去磁盘中查找,如果磁盘中也没有查到就去网络下载。下载一个图片后,先将bitmap对象缓存到磁盘中,再缓存到内存中,如下图所示。


    

本文主要讨论内存缓存,涉及的是LRU算法,即近期最少使用算法,UIL中超出缓存的图片数量的MaxSize,如果超过了这个数量,就将最近最少使用的图片有限删除。代码如下;

    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.LinkedHashMap;
import java.util.Map;

/**
 * 实现MemoryCache接口,LRU算法内存缓存器
 * 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 MemoryCache {
    /*bitmap对象保存集合 采用LinkedHashMap对象保存*/
    private final LinkedHashMap<String, Bitmap> map;
    /*内存缓存最大值*/
    private final int maxSize;
    /**
     * Size of this cache in bytes
     * 缓存字节数量大小
     */
    private int size;

    /**
     * LRU内存缓存器构造器
     * @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.
     * 根据key和图片  存入到缓存中,该被缓存的图片会移动到队列的头
     */
    @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 Bitmap 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);
            }
            return 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);
    }
}

     首先可以看出是通过LinkedHashMap来实现的Lru算法。LinkedHashMap<String, Bitmap>来缓存,声明的map是强引用类型。其次要明确知道,Lru算法中的“最近使用对象”是指最近使用的get、或者put方法的bitmap对象,LinkedHashMap中的get()方法不仅返回所匹配的值,并且在返回前还会将所匹配的key对应的entry调整在列表中的顺序(LinkedHashMap使用双链表来保存数据),让它处于列表的最后。当然,这种情况必须是在LinkedHashMap中accessOrder==true的情况下才生效的,反之就是get()方法不会改变被匹配的key对应的entry在列表中的位置。

关键在于网络上的解释千篇一律的如下:

    到现在我们就清楚LruMemoryCache使用LinkedHashMap来缓存数据,在LinkedHashMap.get()方法执行后,LinkedHashMap中entry的顺序会得到调整。那么我们怎么保证最近使用的项不会被剔除呢?接下去,让我们看看LruMemoryCache.put(…)。在该方法里面,map.put(key,value)的返回值如果不为空,说明存在跟key对应的entry,put操作只是更新原有key对应的entry而已。trimToSize(…)这个函数就是用来限定LruMemoryCache的大小不要超过用户限定的大小,cache的大小由用户在LruMemoryCache刚开始初始化的时候限定。这个函数做的事情也简单,遍历map,将多余的项(代码中对应toEvict)剔除掉,直到当前cache的大小等于或小于限定的大小。

    这时候我们会有一个以为,为什么遍历一下就可以将使用最少的bitmap缓存给剔除,不会误删到最近使用的bitmap缓存吗?首先,我们要清楚,LruMemoryCache定义的最近使用是指最近用get或put方式操作到的bitmap缓存。其次,之前我们直到LruMemoryCache的get操作其实是通过其内部字段LinkedHashMap.get(…)实现的,当LinkedHashMap的accessOrder==true时,每一次get或put操作都会将所操作项(图中第3项)移动到链表的尾部(见下图,链表头被认为是最少使用的,链表尾被认为是最常使用的。),每一次操作到的项我们都认为它是最近使用过的,当内存不够的时候被剔除的优先级最低。需要注意的是一开始的LinkedHashMap链表是按插入的顺序构成的,也就是第一个插入的项就在链表头,最后一个插入的就在链表尾。假设只要剔除图中的1,2项就能让LruMemoryCache小于原先限定的大小,那么我们只要从链表头遍历下去(从1→最后一项)那么就可以剔除使用最少的项了。


        实际上面的解释就是Lru算法实现的过程,其核心就是LinkedHashMap。那为什么遍历了一次LinkedHashMap就会把最近少使用的对象删除呢?实际上就是LinkedHashMap的实现原理,LinkedHashMap是实现Lru算法的关键是accessorder=ture,这个是LinkedHashMap的内部原理,有兴趣的同学可以去看下,简单说就是map超过了限定的大小后,由于最近使用的都在链表的尾部,最少使用的在表的头部,因此遍历map后,超出范围的item就不在这个map里面了。

猜你喜欢

转载自blog.csdn.net/ezview_uniview/article/details/80820205