Android 开源框架Universal-Image-Loader完全解析(二)--- 图片缓存策略详解

时间:2023-03-08 17:02:53
Android 开源框架Universal-Image-Loader完全解析(二)--- 图片缓存策略详解

转载请注明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/26810303),请尊重他人的辛勤劳动成果,谢谢!

本篇文章继续为大家介绍Universal-Image-Loader这个开源的图片加载框架,介绍的是图片缓存策略方面的,如果大家对这个开源框架的使用还不了解,大家可以看看我之前写的一篇文章Android 开源框架Universal-Image-Loader完全解析(一)--- 基本介绍及使用,我们一般去加载大量的图片的时候,都会做缓存策略,缓存又分为内存缓存和硬盘缓存,我之前也写了几篇异步加载大量图片的文章,使用的内存缓存是LruCache这个类,LRU是Least Recently Used 近期最少使用算法,我们可以给LruCache设定一个缓存图片的最大值,它会自动帮我们管理好缓存的图片总大小是否超过我们设定的值, 超过就删除近期最少使用的图片,而作为一个强大的图片加载框架,Universal-Image-Loader自然也提供了多种图片的缓存策略,下面就来详细的介绍下

内存缓存

 

首先我们来了解下什么是强引用和什么是弱引用?

强引用是指创建一个对象并把这个对象赋给一个引用变量, 强引用有引用变量指向时永远不会被垃圾回收。即使内存不足的时候宁愿报OOM也不被垃圾回收器回收,我们new的对象都是强引用

弱引用通过weakReference类来实现,它具有很强的不确定性,如果垃圾回收器扫描到有着WeakReference的对象,就会将其回收释放内存

现在我们来看Universal-Image-Loader有哪些内存缓存策略

1. 只使用的是强引用缓存

  • LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用,下面我会从源码上面分析这个类)

2.使用强引用和弱引用相结合的缓存有

  • UsingFreqLimitedMemoryCache(如果缓存的图片总量超过限定值,先删除使用频率最小的bitmap)
  • LRULimitedMemoryCache(这个也是使用的lru算法,和LruMemoryCache不同的是,他缓存的是bitmap的弱引用)
  • FIFOLimitedMemoryCache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap)
  • LargestLimitedMemoryCache(当超过缓存限定值,先删除最大的bitmap对象)
  • LimitedAgeMemoryCache(当 bitmap加入缓存中的时间超过我们设定的值,将其删除)

3.只使用弱引用缓存

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

上面介绍了Universal-Image-Loader所提供的所有的内存缓存的类,当然我们也可以使用我们自己写的内存缓存类,我们还要看看要怎么将这些内存缓存加入到我们的项目中,我们只需要配置ImageLoaderConfiguration.memoryCache(...),如下

ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)
        .memoryCache(new WeakMemoryCache())
        .build();

  下面我们来分析LruMemoryCache这个类的源代码

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);
    }
}

  我们可以看到这个类中维护的是一个LinkedHashMap,在LruMemoryCache构造函数中我们可以看到,我们为其设置了一个缓存图片的最大值maxSize,并实例化LinkedHashMap, 而从LinkedHashMap构造函数的第三个参数为ture,表示它是按照访问顺序进行排序的,
我们来看将bitmap加入到LruMemoryCache的方法put(String key, Bitmap value),  第61行,sizeOf()是计算每张图片所占的byte数,size是记录当前缓存bitmap的总大小,如果该key之前就缓存了bitmap,我们需要将之前的bitmap减掉去,接下来看trimToSize()方法,我们直接看86行,如果当前缓存的bitmap总数小于设定值maxSize,不做任何处理,如果当前缓存的bitmap总数大于maxSize,删除LinkedHashMap中的第一个元素,size中减去该bitmap对应的byte数

我们可以看到该缓存类比较简单,逻辑也比较清晰,如果大家想知道其他内存缓存的逻辑,可以去分析分析其源码,在这里我简单说下FIFOLimitedMemoryCache的实现逻辑,该类使用的HashMap来缓存bitmap的弱引用,然后使用LinkedList来保存成功加入到FIFOLimitedMemoryCache的bitmap的强引用,如果加入的FIFOLimitedMemoryCache的bitmap总数超过限定值,直接删除LinkedList的第一个元素,所以就实现了先进先出的缓存策略,其他的缓存都类似,有兴趣的可以去看看。

硬盘缓存

接下来就给大家分析分析硬盘缓存的策略,这个框架也提供了几种常见的缓存策略,当然如果你觉得都不符合你的要求,你也可以自己去扩展

  • FileCountLimitedDiscCache(可以设定缓存图片的个数,当超过设定值,删除掉最先加入到硬盘的文件)
  • LimitedAgeDiscCache(设定文件存活的最长时间,当超过这个值,就删除该文件)
  • TotalSizeLimitedDiscCache(设定缓存bitmap的最大值,当超过这个值,删除最先加入到硬盘的文件)
  • UnlimitedDiscCache(这个缓存类没有任何的限制)

下面我们就来分析分析TotalSizeLimitedDiscCache的源码实现

/*******************************************************************************
 * Copyright 2011-2013 Sergey Tarasevich
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package com.nostra13.universalimageloader.cache.disc.impl;  

import com.nostra13.universalimageloader.cache.disc.LimitedDiscCache;
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
import com.nostra13.universalimageloader.core.DefaultConfigurationFactory;
import com.nostra13.universalimageloader.utils.L;  

import java.io.File;  

/**
 * Disc cache limited by total cache size. If cache size exceeds specified limit then file with the most oldest last
 * usage date will be deleted.
 *
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
 * @see LimitedDiscCache
 * @since 1.0.0
 */
public class TotalSizeLimitedDiscCache extends LimitedDiscCache {  

    private static final int MIN_NORMAL_CACHE_SIZE_IN_MB = 2;
    private static final int MIN_NORMAL_CACHE_SIZE = MIN_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024;  

    /**
     * @param cacheDir     Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's
     *                     needed for right cache limit work.
     * @param maxCacheSize Maximum cache directory size (in bytes). If cache size exceeds this limit then file with the
     *                     most oldest last usage date will be deleted.
     */
    public TotalSizeLimitedDiscCache(File cacheDir, int maxCacheSize) {
        this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator(), maxCacheSize);
    }  

    /**
     * @param cacheDir          Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's
     *                          needed for right cache limit work.
     * @param fileNameGenerator Name generator for cached files
     * @param maxCacheSize      Maximum cache directory size (in bytes). If cache size exceeds this limit then file with the
     *                          most oldest last usage date will be deleted.
     */
    public TotalSizeLimitedDiscCache(File cacheDir, FileNameGenerator fileNameGenerator, int maxCacheSize) {
        super(cacheDir, fileNameGenerator, maxCacheSize);
        if (maxCacheSize < MIN_NORMAL_CACHE_SIZE) {
            L.w("You set too small disc cache size (less than %1$d Mb)", MIN_NORMAL_CACHE_SIZE_IN_MB);
        }
    }  

    @Override
    protected int getSize(File file) {
        return (int) file.length();
    }
}

  这个类是继承LimitedDiscCache,除了两个构造函数之外,还重写了getSize()方法,返回文件的大小,接下来我们就来看看LimitedDiscCache

/*******************************************************************************
 * Copyright 2011-2013 Sergey Tarasevich
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package com.nostra13.universalimageloader.cache.disc;  

import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
import com.nostra13.universalimageloader.core.DefaultConfigurationFactory;  

import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;  

/**
 * Abstract disc cache limited by some parameter. If cache exceeds specified limit then file with the most oldest last
 * usage date will be deleted.
 *
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
 * @see BaseDiscCache
 * @see FileNameGenerator
 * @since 1.0.0
 */
public abstract class LimitedDiscCache extends BaseDiscCache {  

    private static final int INVALID_SIZE = -1;  

    //记录缓存文件的大小
    private final AtomicInteger cacheSize;
    //缓存文件的最大值
    private final int sizeLimit;
    private final Map<File, Long> lastUsageDates = Collections.synchronizedMap(new HashMap<File, Long>());  

    /**
     * @param cacheDir  Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's
     *                  needed for right cache limit work.
     * @param sizeLimit Cache limit value. If cache exceeds this limit then file with the most oldest last usage date
     *                  will be deleted.
     */
    public LimitedDiscCache(File cacheDir, int sizeLimit) {
        this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator(), sizeLimit);
    }  

    /**
     * @param cacheDir          Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's
     *                          needed for right cache limit work.
     * @param fileNameGenerator Name generator for cached files
     * @param sizeLimit         Cache limit value. If cache exceeds this limit then file with the most oldest last usage date
     *                          will be deleted.
     */
    public LimitedDiscCache(File cacheDir, FileNameGenerator fileNameGenerator, int sizeLimit) {
        super(cacheDir, fileNameGenerator);
        this.sizeLimit = sizeLimit;
        cacheSize = new AtomicInteger();
        calculateCacheSizeAndFillUsageMap();
    }  

    /**
     * 另开线程计算cacheDir里面文件的大小,并将文件和最后修改的毫秒数加入到Map中
     */
    private void calculateCacheSizeAndFillUsageMap() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                int size = 0;
                File[] cachedFiles = cacheDir.listFiles();
                if (cachedFiles != null) { // rarely but it can happen, don't know why
                    for (File cachedFile : cachedFiles) {
                        //getSize()是一个抽象方法,子类自行实现getSize()的逻辑
                        size += getSize(cachedFile);
                        //将文件的最后修改时间加入到map中
                        lastUsageDates.put(cachedFile, cachedFile.lastModified());
                    }
                    cacheSize.set(size);
                }
            }
        }).start();
    }  

    /**
     * 将文件添加到Map中,并计算缓存文件的大小是否超过了我们设置的最大缓存数
     * 超过了就删除最先加入的那个文件
     */
    @Override
    public void put(String key, File file) {
        //要加入文件的大小
        int valueSize = getSize(file);  

        //获取当前缓存文件大小总数
        int curCacheSize = cacheSize.get();
        //判断是否超过设定的最大缓存值
        while (curCacheSize + valueSize > sizeLimit) {
            int freedSize = removeNext();
            if (freedSize == INVALID_SIZE) break; // cache is empty (have nothing to delete)
            curCacheSize = cacheSize.addAndGet(-freedSize);
        }
        cacheSize.addAndGet(valueSize);  

        Long currentTime = System.currentTimeMillis();
        file.setLastModified(currentTime);
        lastUsageDates.put(file, currentTime);
    }  

    /**
     * 根据key生成文件
     */
    @Override
    public File get(String key) {
        File file = super.get(key);  

        Long currentTime = System.currentTimeMillis();
        file.setLastModified(currentTime);
        lastUsageDates.put(file, currentTime);  

        return file;
    }  

    /**
     * 硬盘缓存的清理
     */
    @Override
    public void clear() {
        lastUsageDates.clear();
        cacheSize.set(0);
        super.clear();
    }  

    /**
     * 获取最早加入的缓存文件,并将其删除
     */
    private int removeNext() {
        if (lastUsageDates.isEmpty()) {
            return INVALID_SIZE;
        }
        Long oldestUsage = null;
        File mostLongUsedFile = null;  

        Set<Entry<File, Long>> entries = lastUsageDates.entrySet();
        synchronized (lastUsageDates) {
            for (Entry<File, Long> entry : entries) {
                if (mostLongUsedFile == null) {
                    mostLongUsedFile = entry.getKey();
                    oldestUsage = entry.getValue();
                } else {
                    Long lastValueUsage = entry.getValue();
                    if (lastValueUsage < oldestUsage) {
                        oldestUsage = lastValueUsage;
                        mostLongUsedFile = entry.getKey();
                    }
                }
            }
        }  

        int fileSize = 0;
        if (mostLongUsedFile != null) {
            if (mostLongUsedFile.exists()) {
                fileSize = getSize(mostLongUsedFile);
                if (mostLongUsedFile.delete()) {
                    lastUsageDates.remove(mostLongUsedFile);
                }
            } else {
                lastUsageDates.remove(mostLongUsedFile);
            }
        }
        return fileSize;
    }  

    /**
     * 抽象方法,获取文件大小
     * @param file
     * @return
     */
    protected abstract int getSize(File file);
}

  在构造方法中,第69行有一个方法calculateCacheSizeAndFillUsageMap(),该方法是计算cacheDir的文件大小,并将文件和文件的最后修改时间加入到Map中

然后是将文件加入硬盘缓存的方法put(),在106行判断当前文件的缓存总数加上即将要加入缓存的文件大小是否超过缓存设定值,如果超过了执行removeNext()方法,接下来就来看看这个方法的具体实现,150-167中找出最先加入硬盘的文件,169-180中将其从文件硬盘中删除,并返回该文件的大小,删除成功之后成员变量cacheSize需要减掉改文件大小。

FileCountLimitedDiscCache这个类实现逻辑跟TotalSizeLimitedDiscCache是一样的,区别在于getSize()方法,前者返回1,表示为文件数是1,后者返回文件的大小。

等我写完了这篇文章,我才发现FileCountLimitedDiscCache和TotalSizeLimitedDiscCache在最新的源码中已经删除了,加入了LruDiscCache,由于我的是之前的源码,所以我也不改了,大家如果想要了解LruDiscCache可以去看最新的源码,我这里就不介绍了,还好内存缓存的没变化,下面分析的是最新的源码中的部分,我们在使用中可以不自行配置硬盘缓存策略,直接用DefaultConfigurationFactory中的就行了

我们看DefaultConfigurationFactory这个类的createDiskCache()方法

/**
 * Creates default implementation of {@link DiskCache} depends on incoming parameters
 */
public static DiskCache createDiskCache(Context context, FileNameGenerator diskCacheFileNameGenerator,
        long diskCacheSize, int diskCacheFileCount) {
    File reserveCacheDir = createReserveDiskCacheDir(context);
    if (diskCacheSize > 0 || diskCacheFileCount > 0) {
        File individualCacheDir = StorageUtils.getIndividualCacheDirectory(context);
        LruDiscCache diskCache = new LruDiscCache(individualCacheDir, diskCacheFileNameGenerator, diskCacheSize,
                diskCacheFileCount);
        diskCache.setReserveCacheDir(reserveCacheDir);
        return diskCache;
    } else {
        File cacheDir = StorageUtils.getCacheDirectory(context);
        return new UnlimitedDiscCache(cacheDir, reserveCacheDir, diskCacheFileNameGenerator);
    }
}

  如果我们在ImageLoaderConfiguration中配置了diskCacheSize和diskCacheFileCount,他就使用的是LruDiscCache,否则使用的是UnlimitedDiscCache,在最新的源码中还有一个硬盘缓存类可以配置,那就是LimitedAgeDiscCache,可以在ImageLoaderConfiguration.diskCache(...)配置

今天就给大家分享到这里,有不明白的地方在下面留言,我会尽量为大家解答的,下一篇文章我将继续更深入的分析这个框架,希望大家继续关注!