Android源码剖析Android开发经验谈Android开发

Android-Universal-Image-Loader源码

2017-12-28  本文已影响15人  正规程序员

ImageLoader简单使用

ImageLoader的二级分包主要由cache、core和utils组成,三者分别负责缓存、核心加载类和工具类。
在核心加载类core的子分包中,包括显示、下载、进度监听及属性配置等。

外部调用:

ImageLoader.getInstance().displayImage(url, imageShow);

这里底层代码由displayImage方法负责:

public void displayImage(
            String uri,
            ImageAware imageAware, 
            DisplayImageOptions options,
            ImageSize targetSize, 
            ImageLoadingListener listener, 
            ImageLoadingProgressListener progressListener) {
...
}

ImageAware:ImageAware这个类会将ImageView转换成ImageViewAware, ImageViewAware主要是做什么的呢?该类主要是将ImageView进行一个包装,将ImageView的强引用变成弱引用,当内存不足的时候,可以更好的回收ImageView对象,还有就是获取ImageView的宽度和高度。这使得我们可以根据ImageView的宽高去对图片进行一个裁剪,减少内存的使用。

通过memoryCacheKey判断内存缓存中是否有bitmap。

设置图片缓存时要做的事

为了能够选择一个合适的缓存大小给缓存类, 有以下多个因素应该放入考虑范围内,例如:

摘自:Android高效加载大图、多图解决方案,有效避免程序OOM

Listview快速滑动不加载图片策略

在PauseOnScrollListener类中,通过监听状态OnScrollListener.SCROLL_STATE_FLING执行imagleloader.pause方法,
最后调用ImageLoaderEngine类的原子变量pause。

LoadAndDisplayImageTask类中:根据原子变量pause的状态对象锁控制阻塞。

/** @return <b>true</b> - if task should be interrupted; <b>false</b> - otherwise */
    private boolean waitIfPaused() {
        AtomicBoolean pause = engine.getPause();
        if (pause.get()) {
            synchronized (engine.getPauseLock()) {
                if (pause.get()) {
                    L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey);
                    try {
                        engine.getPauseLock().wait();
                    } catch (InterruptedException e) {
                        L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
                        return true;
                    }
                    L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);
                }
            }
        }
        return isTaskNotActual();
    }

PauseOnScrollListener类中:监听滑动状态,并根据scroll状态改变ImageLoaderEngine类的原子变量pause值。

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        switch (scrollState) {
            case OnScrollListener.SCROLL_STATE_IDLE:
                imageLoader.resume();
                break;
            case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
                if (pauseOnScroll) {
                    imageLoader.pause();
                }
                break;
            case OnScrollListener.SCROLL_STATE_FLING:
                if (pauseOnFling) {
                    imageLoader.pause();
                }
                break;
        }
        if (externalListener != null) {
            externalListener.onScrollStateChanged(view, scrollState);
        }
    }
 

AtomicBoolean是java.util.concurrent.atomic包下的原子变量,这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入。

ReentrantLock锁机制

典型写法:

Lock lock = new ReentrantLock();
lock.lock();
try {
    // update object state
}finally {
    lock.unlock();
}

Lock是基于JDK层面实现的,通常需要在finally中进行锁的释放,如果忘记就会造成死锁。
ReentrantLock实现Lock接口,所有的同步操作都是依靠AbstractQueuedSynchronizer(队列同步器)实现。

API 如下:public void java.util.concurrent.locks.ReentrantLock.lock()

功能:获取锁。
如果该锁没有被另一个线程保持,则获取该锁并立即返回,并将锁的保持计数器设置为1.
如果当前线程已经保持该锁,则将保持计数加1,并且该方法立即返回。
如果该锁被另一个线程保持,则出于线程调度的目的,禁用该线程,并且在获得锁之前,该线程一直处于休眠状态,此时锁保持计数被设置为1.

公平锁(FairSync)与非公平锁(NonfairSync):如果获取一个锁是按照请求的顺序得到的,那么就是公平锁,否则就是非公平的。

详细的介绍我们在另一篇博客里阐述。todo

内存缓存策略

LruMemoryCache

这个类是通过LinkedHashMap有序的保存着强引用bitmap,是Android-Universal-Image-Loader框架默认的缓存类。

特点

在开源库中,DefaultConfigurationFactory类下的createMemoryCache方法配置缓存类的初始化。

    /**
     * Creates default implementation of {@link MemoryCache} - {@link LruMemoryCache}<br />
     * Default cache size = 1/8 of available app memory.
     */
    public static MemoryCache createMemoryCache(Context context, int memoryCacheSize) {
        if (memoryCacheSize == 0) {
            ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
            //获取可使用的最大内存,单位MB
            int memoryClass = am.getMemoryClass();
            if (hasHoneycomb() && isLargeHeap(context)) {
                memoryClass = getLargeMemoryClass(am);
            }
            //将最大内存的1/8转变成字节单位。
            memoryCacheSize = 1024 * 1024 * memoryClass / 8;
        }
        return new LruMemoryCache(memoryCacheSize);
    }

1MB = 1024KB = 1024* 1024B(字节)。1B = 8bit(8位,即8位二进制数)。

“删除最早缓存的”代码如下:

/**
     * 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;
                }

                //这是在size > maxSize的情况下,通过Map.entrySet使用iterator遍历key和value,每次都是先获取第一个。remove之后循环下一次。
                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);
            }
        }
    }

在size > maxSize的情况下,通过Map.entrySet使用iterator遍历key和value,每次都是先获取第一个。remove之后循环下一次。

UsingFreqLimitedMemoryCache

这个类是通过对已存储的bitmap的使用率进行比对,从而移除最小使用率的bitmap,达到控制内存的目的。

特点:

UsingFreqLimitedMemoryCache继承了父类LimitedMemoryCache,LimitedMemoryCache继承父类BaseMemoryCache。
UsingFreqLimitedMemoryCache中的数据结构是:

/**
     * Contains strong references to stored objects (keys) and last object usage date (in milliseconds). If hard cache
     * size will exceed limit then object with the least frequently usage is deleted (but it continue exist at
     * {@link #softMap} and can be collected by GC at any time)
     */
    private final Map<Bitmap, Integer> usingCounts = Collections.synchronizedMap(new HashMap<Bitmap, Integer>());

map中的key是bitmap,表示存储的位图;value是Integer,表示使用的次数。
Collections.synchronizedMap,解决了线程安全性问题。 通过将基本的功能从线程安全性中分离开来,允许需要同步的用户可以拥有同步,而不需要同步的用户则不必为同步付出代价。
LimitedMemoryCache中的数据结构是:

private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());

此类主要实现一些获取可用内存字节和移除使用率最低的抽象方法。(本来我认为此类可舍弃,该类的逻辑代码可在BaseMemoryCache中进行,但是看到LRULimitedMemoryCache类之后,觉得我的思想设计扩展性很不强哦)
BaseMemoryCache中的数据结构是:

private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());

这里是将value对应的bitmap放在引用的泛型中,并在UsingFreqLimitedMemoryCache类中实现抽象方法createReference()。

因为WeakReference< T >等都是以此形式继承Reference< T >,如此便于扩展。

UsingFreqLimitedMemoryCache中:

Override
protected Reference<Bitmap> createReference(Bitmap value) {
    return new WeakReference<Bitmap>(value);
}

因此,存储的bitmap是以弱引用的形式存储在hashmap中。

put:

Override
    public boolean put(String key, Bitmap value) {
        if (super.put(key, value)) {
            usingCounts.put(value, 0);
            return true;
        } else {
            return false;
        }
    }

这里在对map进行put值的时候,会默认将表示使用次数的Integer设置为0;而在get的时候会将其+1;
在第二行里子类调用了父类的put方法,其中包含了对removeNext()的调用,是为了处理当存储超出上限时移除使用率最小的值。

get:

    @Override
    public Bitmap get(String key) {
        Bitmap value = super.get(key);
        // Increment usage count for value if value is contained in hardCahe
        if (value != null) {
            Integer usageCount = usingCounts.get(value);
            if (usageCount != null) {
                usingCounts.put(value, usageCount + 1);
            }
        }
        return value;
    }

removeNext():

    @Override
    protected Bitmap removeNext() {
        Integer minUsageCount = null;
        Bitmap leastUsedValue = null;
        Set<Entry<Bitmap, Integer>> entries = usingCounts.entrySet();
        synchronized (usingCounts) {
            for (Entry<Bitmap, Integer> entry : entries) {
                if (leastUsedValue == null) {
                    leastUsedValue = entry.getKey();
                    minUsageCount = entry.getValue();
                } else {
                    Integer lastValueUsage = entry.getValue();
                    if (lastValueUsage < minUsageCount) {
                        minUsageCount = lastValueUsage;
                        leastUsedValue = entry.getKey();
                    }
                }
            }
        }
        //检索完毕之后,移除使用频率最低的那个值。
        usingCounts.remove(leastUsedValue);
        return leastUsedValue;
    }

这里,lastValueUsage表示本次(最后、最新)循环内的使用次数值,minUsageCount不为null时表示上次循环时赋的值。
如果检索出更小使用次数时,则将该循环内的entry.getKey()和entry.getValue()赋值给leastUsedValue和minUsageCount。

LRULimitedMemoryCache

此类和UsingFreqLimitedMemoryCache都是继承父类LimitedMemoryCache,意味着上层的逻辑处理是一样的。

特点:

可以理解为只是将LruMemoryCache中的bitmap由强引用转化为弱引用。

此类的数据结构是:

    /** Cache providing Least-Recently-Used logic */
    private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));

该类没有统计使用率的Integer的value,但有LinkedHashMap组成的有序的结构体,LinkedHashMap是一个双向循环链表。
后续会有章节详述Java数据结构及原理,此处略。
removeNext:

    @Override
    protected Bitmap removeNext() {
        Bitmap mostLongUsedValue = null;
        synchronized (lruCache) {
            //移除最早的那个,因为数据结构是LinkedHashMap,存储是有序的。
            Iterator<Entry<String, Bitmap>> it = lruCache.entrySet().iterator();
            if (it.hasNext()) {
                Entry<String, Bitmap> entry = it.next();
                mostLongUsedValue = entry.getValue();
                it.remove();
            }
        }
        return mostLongUsedValue;
    }

既然LRULimitedMemoryCache和UsingFreqLimitedMemoryCache都是继承父类LimitedMemoryCache,我们看看父类的方法吧。

在LimitedMemoryCache中,

    @Override
    public boolean put(String key, Bitmap value) {
        boolean putSuccessfully = false;
        // Try to add value to hard cache
        int valueSize = getSize(value);
        int sizeLimit = getSizeLimit();
        int curCacheSize = cacheSize.get();
        if (valueSize < sizeLimit) {
            while (curCacheSize + valueSize > sizeLimit) {
                Bitmap removedValue = removeNext();
                //硬盘缓存移除使用频率最低的bitmap
                if (hardCache.remove(removedValue)) {
                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                }
            }
            //硬盘缓存添加
            hardCache.add(value);
            cacheSize.addAndGet(valueSize);

            putSuccessfully = true;
        }
        // Add value to soft cache
        super.put(key, value);
        return putSuccessfully;
    }

在当前已缓存的字节+此次要缓存的bitmap的字节之和大于缓存字节上限时,清除子类指定的bitmap。

FIFOLimitedMemoryCache

此类和UsingFreqLimitedMemoryCache都是继承父类LimitedMemoryCache,意味着上层的逻辑处理是一样的。

特点:

此类的数据结构与之前的不同:

private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());

LinkedList列表先进先出的方式存储bitmap。

Override
    protected Bitmap removeNext() {
        return queue.remove(0);
    }

超出内存存储上限时的处理也是直接移除先进去(第一个)的bitmap。

LargestLimitedMemoryCache

此类和UsingFreqLimitedMemoryCache都是继承父类LimitedMemoryCache,意味着上层的逻辑处理是一样的。

特点:

此类的数据结构是:

    /**
     * Contains strong references to stored objects (keys) and sizes of the objects. If hard cache
     * size will exceed limit then object with the largest size is deleted (but it continue exist at
     * {@link #softMap} and can be collected by GC at any time)
     */
    private final Map<Bitmap, Integer> valueSizes = Collections.synchronizedMap(new HashMap<Bitmap, Integer>());

疑问:既然都是继承父类LimitedMemoryCache,也都采用强引用和弱引用相结合的方式实现存储。那么三者最后是将bitmap以强引用bitmap、弱引用bitmap、LinkedList中bitmap这其中哪种方式存储呢?

答:是以子类中createReference(Bitmap value)方法指定的引用类型存储的,目前三者都是以WeakReference弱引用的形式存储在基类BaseMemoryCache定义的HashMap中。
依据:子类中的get和put方法都是调用基类BaseMemoryCache的方法,而子类自己的数据结构以及父类LimitedMemoryCache的是以一种便于操作数据的数据结构中存在的(因为弱引用的生命周期不定)。

LimitedAgeMemoryCache

特点:

结构体如下:

private final Map<String, Long> loadingDates = Collections.synchronizedMap(new HashMap<String, Long>());

get

    @Override
    public Bitmap get(String key) {
        Long loadingDate = loadingDates.get(key);
        Bitmap bitmap = cache.get(key);
        //在获取值的时候检测时间是否超出缓存时限,若超出则移除。
        if (loadingDate != null && System.currentTimeMillis() - loadingDate > maxAge) {
            cache.remove(key);
            loadingDates.remove(key);
        }
        return bitmap;
    }

这里我做了修改,增加了Bitmap bitmap = cache.get(key);

WeakMemoryCache

此类直接继承BaseMemoryCache实现弱引用bitmap存储。

特点:

类结构如下:

/**
 * Memory cache with {@linkplain WeakReference weak references} to {@linkplain Bitmap bitmaps}<br />
 * <br />
 * <b>NOTE:</b> This cache uses only weak references for stored Bitmaps.
 *
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
 * @since 1.5.3
 */
public class WeakMemoryCache extends BaseMemoryCache {
    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉。

LruCache

先看下构造方法:

   /**
     * LruCache的构造方法:需要传入最大缓存个数
     */
    public LruCache(int maxSize) {
        // 最大缓存个数小于0,会抛出IllegalArgumentException
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        /*
         * 初始化LinkedHashMap
         * 第一个参数:initialCapacity,初始大小
         * 第二个参数:loadFactor,负载因子=0.75f
         * 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
         */
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

这里设置了最大缓存的字节数,一般是16M10241024B,然后初始化了LinkedHashMap的参数,其中accessOrder=true,表示基于访问顺序进行排序的。

new LinkedHashMap<K, V> 中的键值对是通过类的泛型传入的。比如图片加载,可以用new LinkedHashMap<String, Bitmap>作为键值对。

在put方法里

  /**
     * 给对应key缓存value,该value将被移动到队头。
     */
    public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            // 记录 put 的次数
            putCount++;
            // 拿到键值对,计算出在容量中的相对长度,然后加上
            size += safeSizeOf(key, value);
            /*
             * 放入 key value
             * 如果 之前存在key 则返回 之前key 的value
             * 记录在 previous
             */
            previous = map.put(key, value);
            // 如果存在冲突
            if (previous != null) {
                // 计算出 冲突键值 在容量中的相对长度,然后减去
                size -= safeSizeOf(key, previous);
            }
        }

        // 如果上面发生冲突
        if (previous != null) {
            /*
             * previous值被剔除了,此次添加的 value 已经作为key的 新值
             * 告诉 自定义 的 entryRemoved 方法
             */
            entryRemoved(false, key, previous, value);
        }
        trimToSize(maxSize);
        return previous;
    }

和以往的缓存类相似。在同步方法块内,先计算要put的value的大小(字节),size累加,然后map.put(),该方法的返回值若不为null,表明替换掉同一个key的旧的value值了。并且size减去旧值的大小(字节)。

同步块下,有个entryRemoved方法,这个方法默认没有实现,暂做剔除的自定义处理。

每次put 新值的时候就要检测是否超出最大缓存空间。这个步骤是在trimToSize方法内进行。

trimToSize如下:

 /**
     * 删除最旧的数据直到剩余的数据的总数以下要求的大小。
     */
    public void trimToSize(int maxSize) {
        /*
         * 这是一个死循环,
         * 1.只有 扩容 的情况下能立即跳出
         * 2.非扩容的情况下,map的数据会一个一个删除,直到map里没有值了,就会跳出
         */
        while (true) {
            K key;
            V value;
            synchronized (this) {
                // 在重新调整容量大小前,本身容量就为空的话,会出异常的。
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(
                            getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                }
                // 如果是 扩容 或者 map为空了,就会中断,因为扩容不会涉及到丢弃数据的情况
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
                //剩下的情况就是 if (size > maxSize && !map.isEmpty())即存储已超出最大缓存空间

                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                // 拿到键值对,计算出在容量中的相对长度,然后减去。
                size -= safeSizeOf(key, value);
                // 添加一次收回次数
                evictionCount++;
            }
            /*
             * 将最后一次删除的最少访问数据回调出去
             */
            entryRemoved(true, key, value, null);
        }
    }

这里检测插入的值是否超出最大缓存空间,如果超出了就会执行移除操作。

while循环中遍历进行,符合条件(超出最大缓存空间时)就会执行 map.remove(key)。由于LinkedHashMap是按照访问顺序排序的,所以此缓存策略是按照“最近最少使用”算法(在链表的尾部是最近刚刚使用的结点,在链表的头部是是最近最少使用的结点)进行剔除的。

为了实现多线程中的同步,对缓存的操作是放在同步块中进行的。

我们再看下get方法:

 /**
     * 根据key查询缓存,如果存在于缓存或者被create方法创建了。
     * 如果值返回了,那么它将被移动到双向循环链表的的尾部。
     * 如果如果没有缓存的值,则返回null。
     */
    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            // LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
            mapValue = map.get(key);
            // 计算 命中次数
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            // 计算 丢失次数
            missCount++;
        }

        /*
         * 官方解释:
         * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
         * 候,一个冲突的值被添加到Map,我们在Map中删除这个值,释放被创造的值。
         */
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        /***************************
         * 不覆写create方法走不到下面 *
         ***************************/

        /*
         * 正常情况走不到这里
         * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
         * 因为默认的 create(K key) 逻辑为null
         */
        synchronized (this) {
            // 记录 create 的次数
            createCount++;
            // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
            mapValue = map.put(key, createdValue);

            // 如果之前存在相同key的value,即有冲突。
            if (mapValue != null) {
                /*
                 * 有冲突
                 * 所以 撤销 刚才的 操作
                 * 将 之前相同key 的值 重新放回去
                 */
                map.put(key, mapValue);
            } else {
                // 拿到键值对,计算出在容量中的相对长度,然后加上
                size += safeSizeOf(key, createdValue);
            }
        }

        // 如果上面 判断出了 将要放入的值发生冲突
        if (mapValue != null) {
            /*
             * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
             * 告诉 自定义 的 entryRemoved 方法
             */
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            // 上面 进行了 size += 操作 所以这里要重整长度
            trimToSize(maxSize);
            return createdValue;
        }
    }

通篇的代码逻辑很复杂,简单说就是如果get不到值,可以在create(key)里自定义一个value,然后put到map中。如果此时检测到map中有该key的旧值,就撤销put之前create的值。同样的,对于所有要剔除的操作都要走entryRemoved方法。

注意:网上下载的源码里的汉语注释,不敢恭维。

DiskLruCache

DiskLruCache构造方法:
DiskLruCache类是因为是常量类,不能通过构造方法实例化,这里通过open方法获取DiskLruCache实例。

    /**
     * Opens the cache in {@code directory}, creating a cache if none exists
     * there.
     *
     * @param directory a writable directory,for example," /sdcard/Android/data/<application package>/cache"
     * @param valueCount the number of values per cache entry. Must be positive.
     * @param maxSize the maximum number of bytes this cache should use to store
     * @param maxFileCount the maximum file count this cache should store
     * @throws IOException if reading or writing the cache directory fails
     */
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)
            throws IOException {
            ...
    }

open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。

这里缓存的位置通常存放在 /sdcard/Android/data/<application package>/cache 这个路径下面,如果没有SD卡或被移除了,可以采用如下代码设置缓存地址。

/**
     * 获取本地文件
     * @param uniqueName 子目录
     * @return
     */
    public File getDiskCacheDir(String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = App.getAppContext().getExternalCacheDir().getPath();
        } else {
            cachePath = App.getAppContext().getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

对于版本号appVersion这个参数,每当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。

外部实例化DiskLruCache如下:

DiskLruCache mDiskLruCache = null;
try {
    File cacheDir = getDiskCacheDir(context, "bitmap");
    if (!cacheDir.exists()) {
        cacheDir.mkdirs();
    }
    mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
} catch (IOException e) {
    e.printStackTrace();
}

DiskLruCache写入:
DiskLruCache写入的操作是借助DiskLruCache.Editor这个类完成的,需要调用DiskLruCache的edit()方法来获取实例。

DiskLruCache.Editor editor = mDiskLruCache.edit(key);  

由于缓存的key要和缓存的value一一对应,这里通常是将URL通过MD5编码后作key。

MD5编码如下:

  /**
     * MD5 算法
     * @param key
     * @return
     */
    public String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

具体编码算法,后续再研究。

获取editor实例后,通过newOutputStream()方法可获得一个输出流OutputStream。然后将此输出流传入网络下载中,即可实现在文件下载的过程中,将资源缓存到本地。

完整的缓存过程如下:

new Thread(new Runnable() {  
    @Override  
    public void run() {  
        try {  
            String imageUrl = "http://a3.topitme.com/8/2d/b7/1128528404720b72d8o.jpg";  
            String key = hashKeyForDisk(imageUrl);  
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);  
            if (editor != null) {  
                OutputStream outputStream = editor.newOutputStream(0);  
                if (downloadImg(imageUrl, outputStream)) {  
                    editor.commit();  
                } else {  
                    editor.abort();  
                }  
            }  
            mDiskLruCache.flush();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}).start();

newOutputStream()方法接收一个index参数,由于前面在设置valueCount的时候指定的是1,所以这里index传0就可以了(即索引为0)。
这里调用一下commit()方法进行提交才能使写入生效,调用abort()方法的话则表示放弃此次写入。

mDiskLruCache.flush()方法是用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候我有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。

DiskLruCache读取:

因为写入缓存的时候是通过MD5编码之后的key作为唯一性标识的,读取的时候也是首先要将URL转码成Key。

String imageUrl = "http://a3.topitme.com/8/2d/b7/1128528404720b72d8o.jpg";  
String key = hashKeyForDisk(imageUrl);  
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); 

这里通过获得DiskLruCache.Snapshot实例,然后调用其getInputStream()就可获得输入流,进而转发成对应的文件,如bitmap。这里getInputStream(0)的参数和newOutputStream(0)的参数是一一对应的。

完整的读取代码如下:

try {  
    String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
    String key = hashKeyForDisk(imageUrl);  
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
    if (snapShot != null) {  
        InputStream is = snapShot.getInputStream(0);  
        Bitmap bitmap = BitmapFactory.decodeStream(is);  
        mImage.setImageBitmap(bitmap);  
    }  
} catch (IOException e) {  
    e.printStackTrace();  
}  

我们在DiskLruCache源码中,会看到其也用到了LinkedHashMap这个结构体

private final LinkedHashMap<String, Entry> lruEntries =new LinkedHashMap<String, Entry>(0, 0.75f, true);

这也是基于访问顺序的“最近最少使用算法”,其中的value是Entry类,表示文件的实体类。因此DiskLruCache和LruCache底层思想都是相同的,只是存储的对象有所不同。

LRU算法(Least Recently Used )

缓存主要由LruCache这个类实现,LRU是Least Recently Used 近期最少使用算法,多用于清理长时间未使用的资源。

通过查看LinkedHashMap源码可知:

 /**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

boolean值 accessOrder参数为true表示按照使用顺序排列,为false表示按照插入顺序排列。

当accessOrder 设置为 true时,每当我们更新(即调用put方法)或访问(即调用get方法)map中的结点时,LinkedHashMap内部都会将这个结点移动到链表的尾部,因此,在链表的尾部是最近刚刚使用的结点,在链表的头部是是最近最少使用的结点,当我们的缓存空间不足时,就应该持续把链表头部结点移除掉,直到有剩余空间放置新结点。

引用

Android-Universal-Image-Loader
LruCache 源码解析
DiskLruCache

上一篇下一篇

猜你喜欢

热点阅读