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

        

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

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

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


    分析Universal-Image-Loader的缓存原理_第1张图片

本文主要讨论内存缓存,涉及的是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.
*
* NOTE: 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 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(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 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 keys() { synchronized (this) { return new HashSet(map.keySet()); } } @Override public void clear() { trimToSize(-1); // -1 will evict 0-sized elements } /** * 进行计算图片大小 * Returns the size {@code Bitmap} in bytes. *

* 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来缓存,声明的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里面了。

你可能感兴趣的:(Android基础)