首页 > 系统 > Android > 正文

Android-Universal-Image-Loader中的缓存分析

2019-11-09 18:10:29
字体:
来源:转载
供稿:网友
Android-Universal-Image-Loader主要是介绍缓存一.该框架的一些主要特点如下:1、支持可定制化,可以更具自己项目的需求进行配置:下载的线程池,图片下载器,内存缓存策略(可自定义)等 2、多线程异步加载和显示图片,图片来源广泛如:网络图片,sdcard图片,assets文件夹下的图片,drawable与mipmap文件夹下的图片,系统里面的视频缩略图(.9patch不能加载) 3、支持图片的内存缓存,sdcard缓存。(图片缓存可以设置,先进先出,使用最少的,占用内存最大的先移除的配置) 4、支持图片加载过程中的监听,可以暂停加载图片,图片在加载中的操作。5、没有对本地文件压缩处理的相关API方法以及默认都是Src模式设置图片,没有针对Background属性开放的api二.缓存的策略:Memory Cache + File Cache 也就是内存的缓存 + “文件缓存”需要注意的是这里面的两种缓存都可以自己来实现其中的具体的缓存方式(也就是怎么在达到缓存的上限的时候进行删除操作)Memory Cache主要的类: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();//抽象方法,由子类实现                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;    }存放一张图片到内存里面的时候,会对当前已经缓存的图片大小总和即将要进行缓存的图片进行求和,通过removeNext()方法进行删除图片,每删除一次进行一次检查,直到总大小小于设定值的大小才进行缓存加载。对于这个removeNext抽象方法由子类来进行实现,删除的方式多种多样,可以按照各种算法随便发挥,了解到这个核思想之后,我们开始来介绍几种实现了这个类的缓存方案。1.LRUMemoryCache: LRU(Least Recently Use),最近最少使用的缓存方式,就是缓存的大小超过一定的值之后,将会删除最近使用最少的那张图片。要使用这个缓存方式,我们先要了解一下LinkedHashMap,链式的哈西表,数据保存时使用的简和值都使用他们对应的哈西编码,然后使用一个双循环链表结构进行顺序维护,HashMap是一个散列存储,都是无序的,读取效率很高。LinkedHashMap在其基础上添加了一个双链表进行顺序的维护。然后我们的LRU算法也基本上都是这个存储结构实现。PRivate final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));要实现这个使用顺序维护,就要使用这个构造方法了,第一个参数是初始化容器大小,但是实际申请的大小空间会是这么一个关系:2的n次方>=INITIAL_CAPACITY , 在满足这个关系的前提下,取n的最小值。在INITIAL_CAPACITY==0的情况下比较特殊,这时候容器大小是2。LOAD_FACTOR是在容器中存放数量超过长度的这个比例时,容器将自动扩容为之前的两倍,第三个参数设置为true就是这个的关键了,把这个设置为true之后,每次在读取某一个容器的持有对象之后,这个持有的对象将会被放到尾部。@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;    }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++;    }方法中的accessorder就是你在构造函数里面传递进去的,只要这个是true,执行了get方法之后,就会把这个函数移动到链表的表尾,有了以上的基础,LRU似乎变得更加简单了。我们需要做的就是在每次缓存判断之后,如果需要删除图片引用,将链表中的第一个元素删除就可以了,因为最近使用的都会被移动到后面。/** 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);            }        }    }在每次图片放进去之后,都会使用trimToSize方法进行容器调整,调整到缓存里面的大小小于穿进来的参数,在这里穿进去的自然就是我们设置的缓存大小。2.FIFOLimitedMemoryCache: 先进先出的一个策略。跟LRU的区别是,他每次都是简单的删除最先加入的元素,就是一个队列的形式,这个就没有上面的那个复杂了,这只需要一个List记录下保存的图片信息,每次删除队列中的第一个就行了:@Override    protected Bitmap removeNext() {        return queue.remove(0);    }这个子类在这个方法的实现上就是这么简单的一个操作,删除第一个元素3.FuzzyKeyMemoryCache:这个是一个key等价删除策略,在构造器中穿一个Comparator参数进来,重写public int compare(T lhs, T rhs);这个方法,这种缓存会把已经缓存的key和将要缓存的key进行自己实现定义的方式进行对比,如果出现了这种等价就删除掉第一个对应的entry。如果没有找到就不用删除。核心代码 :@Override    public boolean put(String key, Bitmap value) {        // Search equal key and remove this entry        synchronized (cache) {            String keyToRemove = null;            for (String cacheKey : cache.keys()) {                if (keyComparator.compare(key, cacheKey) == 0) {                    keyToRemove = cacheKey;                    break;                }            }            if (keyToRemove != null) {                cache.remove(keyToRemove);            }        }        return cache.put(key, value);    }4.LargestLimitedMemoryCache:这个缓存策略是在放入缓存前检测已经缓存的总容量,如果超过了这个值就删除掉占用内存最大的元素,这个就像是以前做的java基础体,在一个数组里面找出最大值一样:@Override    protected Bitmap removeNext() {        Integer maxSize = null;        Bitmap largestValue = null;        Set<Entry<Bitmap, Integer>> entries = valueSizes.entrySet();        synchronized (valueSizes) {            for (Entry<Bitmap, Integer> entry : entries) {                if (largestValue == null) {                    largestValue = entry.getKey();                    maxSize = entry.getValue();                } else {                    Integer size = entry.getValue();                    if (size > maxSize) {                        maxSize = size;                        largestValue = entry.getKey();                    }                }            }        }        valueSizes.remove(largestValue);        return largestValue;    }首先默认把第一个当作最小的,然后循环跟后面的进行比较,出现更大的就进行赋值,这样循环结束之后就可以得到占用内存最大的那个引用了。5.LimitedAgeMemoryCache : 这个是一个时间限制的缓存策略,每次在引用一个对象到内存的时候,使用一个HashMap记录下来key和它存进去的时间,然后在访问缓存中的某张图片的时候,跟当前时间进行一个比较,如果这个两个时间的差值大于设置的缓存时间,就会把图片引用从缓存中删掉。@Override    public boolean put(String key, Bitmap value) {        boolean putSuccesfully = cache.put(key, value);        if (putSuccesfully) {            loadingDates.put(key, System.currentTimeMillis());        }        return putSuccesfully;    }    @Override    public Bitmap get(String key) {        Long loadingDate = loadingDates.get(key);        if (loadingDate != null && System.currentTimeMillis() - loadingDate > maxAge) {            cache.remove(key);            loadingDates.remove(key);        }        return cache.get(key);    }第一个方法在保存的时候,同时保存当前时间。第二个方法在取出的时候,进行时间的检测。6. UsingFreqLimitedMemoryCache : 在缓存超过限制之后,根据使用频率最少的引用进行优先的删除,这个是在保存引用的时候保存的是key和使用次数,放进去的时候记录次数为0,每次get调用次数加1,然后在调用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;    }File CacheUIL中的磁盘缓存策略像新浪微博、花瓣这种应用需要加载很多图片,本来图片的加载就慢了,如果下次打开的时候还需要再一次下载上次已经有过的图片,相信用户的流量会让他们的叫骂声很响亮。对于图片很多的应用,一个好的磁盘缓存直接决定了应用在用户手机的留存时间。我们自己实现磁盘缓存,要考虑的太多,幸好UIL提供了几种常见的磁盘缓存策略,当然如果你觉得都不符合你的要求,你也可以自己去扩展FileCountLimitedDiscCache(可以设定缓存图片的个数,当超过设定值,删除掉最先加入到硬盘的文件)LimitedAgeDiscCache(设定文件存活的最长时间,当超过这个值,就删除该文件)TotalSizeLimitedDiscCache(设定缓存bitmap的最大值,当超过这个值,删除最先加入到硬盘的文件)UnlimitedDiscCache(这个缓存类没有任何的限制)在UIL中有着比较完整的存储策略,根据预先指定的空间大小,使用频率(生命周期),文件个数的约束条件,都有着对应的实现策略。最基础的接口DiscCacheAware和抽象类BaseDiscCache UnlimitedDiscCache解析UnlimitedDiscCache实现disk cache接口,是ImageLoaderConfiguration中默认的磁盘缓存处理。用它的时候,磁盘缓存的大小是不受限的。接下来我们来看看实现UnlimitedDiscCache的源代码,通过源代码我们发现他其实就是继承了BaseDiscCache,这个类内部没有实现自己独特的方法,也没有重写什么,那么我们就直接看BaseDiscCache这个类。在分析这个类之前,我们先想想自己实现一个磁盘缓存需要做多少麻烦的事情:1、图片的命名会不会重。你没有办法知道用户下载的图片原始的文件名是怎么样的,因此很可能因为文件重名将有用的图片给覆盖掉了。2、当应用卡顿或网络延迟的时候,同一张图片反复被下载。3、处理图片写入磁盘可能遇到的延迟和同步问题。 BaseDiscCache构造函数首先,我们看一下BaseDiscCache的构造函数:cacheDir:文件缓存目录reserveCacheDir:备用的文件缓存目录,可以为null。它只有当cacheDir不能用的时候才有用。fileNameGenerator:文件名生成器。为缓存的文件生成文件名。    public BaseDiscCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {        if (cacheDir == null) {            throw new IllegalArgumentException("cacheDir" + ERROR_ARG_NULL);        }        if (fileNameGenerator == null) {            throw new IllegalArgumentException("fileNameGenerator" + ERROR_ARG_NULL);        }        this.cacheDir = cacheDir;        this.reserveCacheDir = reserveCacheDir;        this.fileNameGenerator = fileNameGenerator;    }我们可以看到一个fileNameGenerator,接下来我们来了解UIL具体是怎么生成不重复的文件名的。UIL中有3种文件命名策略,这里我们只对默认的文件名策略进行分析。默认的文件命名策略在DefaultConfigurationFactory.createFileNameGenerator()。它是一个HashCodeFileNameGenerator。真的是你意想不到的简单,就是运用String.hashCode()进行文件名的生成。public class HashCodeFileNameGenerator implements FileNameGenerator {    @Override    public String generate(String imageUri) {        return String.valueOf(imageUri.hashCode());    }} BaseDiscCache.save()分析完了命名策略,再看一下BaseDiscCache.save(...)方法。注意到第2行有一个getFile()函数,它主要用于生成一个指向缓存目录中的文件,在这个函数里面调用了刚刚介绍过的fileNameGenerator来生成文件名。注意第3行的tmpFile,它是用来写入bitmap的临时文件(见第8行),然后就把这个文件给删除了。大家可能会困惑,为什么在save()函数里面没有判断要写入的bitmap文件是否存在的判断,我们不由得要看看UIL中是否有对它进行判断。UIL加载图片的一般流程是先判断内存中是否有对应的Bitmap,再判断磁盘(disk)中是否有,如果没有就从网络中加载。最后根据原先在UIL中的配置判断是否需要缓存Bitmap到内存或磁盘中。也就是说,当需要调用BaseDiscCache.save(...)之前,其实已经判断过这个文件不在磁盘中。    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {         File imageFile = getFile(imageUri);         File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);         boolean loaded = false;         try {             OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);             try {                 loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);             } finally {                IoUtils.closeSilently(os);            }         } finally {             IoUtils.closeSilently(imageStream);             if (loaded && !tmpFile.renameTo(imageFile)) {               loaded = false;           }            if (!loaded) {                tmpFile.delete();             }        }        return loaded;    } BaseDiscCache.get()BaseDiscCache.get()方法内部调用了BaseDiscCache.getFile(...)方法,让我们来分析一下这个在之前碰过的函数。 第2行就是利用fileNameGenerator生成一个唯一的文件名。第3~8行是指定缓存目录,这时候你就可以清楚地看到cacheDir和reserveCacheDir之间的关系了,当cacheDir不可用的时候,就是用reserveCachedir作为缓存目录了。       最后返回一个指向文件的对象,但是要注意当File类型的对象指向的文件不存在时,file会为null,而不是报错。    protected File getFile(String imageUri) {        String fileName = fileNameGenerator.generate(imageUri);         File dir = cacheDir;        if (!cacheDir.exists() && !cacheDir.mkdirs()) {            if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {                 dir = reserveCacheDir;            }        }       return new File(dir, fileName);    }三.总结:灵活的api,定制性比较好,但是停更了,如果没有使用在项目中不太建议使用,但是其中的思想值得学习参考:http://www.cnblogs.com/kissazi2/p/3931400.html和http://blog.csdn.net/u014393454/article/details/49783807
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表