Android开源框架Universal-Image-Loader缓存机制浅析

缓存

提高用户体验,同时也使得应用更加流畅,也就是缓存图片至内存时,可以更加高效的工作。

配置

在应用中配置ImageLoaderConfiguration参数(注意:只配置一次就好了,如多次配置,则默认第一次的配置参数)

默认设置(即框架已配置好了参数)

ImageLoaderConfiguration configuration = ImageLoaderConfiguration.createDefault(this);

自定义设置

File cacheDir = StorageUtils.getCacheDirectory(context);  //缓存文件夹路径
    ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
            .memoryCacheExtraOptions(480, 800) // default = device screen dimensions 内存缓存文件的最大长宽
           // .diskCacheExtraOptions(480, 800, null)  // 本地缓存的详细信息(缓存的最大长宽),最好不要设置这个 
            .taskExecutor(...)
            .taskExecutorForCachedImages(...)
            .threadPoolSize(5) // default是3个线程池  线程池内加载的数量
            .threadPriority(Thread.NORM_PRIORITY - 2) // default 设置当前线程的优先级
            .tasksProcessingOrder(QueueProcessingType.FIFO) // default
            .denyCacheImageMultipleSizesInMemory()
            .memoryCache(new LruMemoryCache(2 * 1024 * 1024)) //可以通过自己的内存缓存实现
            .memoryCacheSize(2 * 1024 * 1024)  // 内存缓存的最大值
            .memoryCacheSizePercentage(13) // default
            .diskCache(new UnlimitedDiscCache(cacheDir)) // default ,可以自定义缓存路径  
            .diskCacheSize(50 * 1024 * 1024) // 50 Mb sd卡(本地)缓存的最大值
            .diskCacheFileCount(100)  // 可以缓存的文件数量 
            // default为使用HASHCODE对UIL进行加密命名, 还可以用MD5(new Md5FileNameGenerator())加密
            .diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) 
            .imageDownloader(new BaseImageDownloader(context)) // default
            .imageDecoder(new BaseImageDecoder()) // default
            .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
            .writeDebugLogs() // 打印debug log
            ImageLoader.getInstance().init(config.build()); //初始化配置,开始构建

主体有三个,分别是UI,缓存模块和数据源(网络)。

UI: 请求数据,使用唯一的Key值索引Memory Cache中的Bitmap。
缓存模块:分两种,一是内存缓存,通过缓存搜索,如果能找到Key值对应的Bitmap,则返回数据。二是硬盘存储,使用唯一Key值对应的文件名,检索SDCard上的文件。

默认的内存缓存实现是LruMemoryCache,磁盘缓存是UnlimitedDiscCache。

  • 先说说 MemoryCache 接口实现,底下有很多实现类
  • 它负责定义通用规则,具体的实现工作由不同缓存算法的子类去实现即可。
  • 这是一个继承和多态的体现。
  • 当前分析的就是 LruMemoryCache 类,这也是默认的缓存策略
  • 内存缓存策略设计与实现
  • 只使用的是强引用缓存
    LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用,下面我会从源码上面分析这个类)

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

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

LruMemoryCach

LruMemoryCach这也是默认的缓存策略

1、最近最少使用算法。
2、当图片缓存到磁盘之后,然后就需要缓存到内存中。
3、需要定义的缓存空间是多大呢?(默认大小为 app 可用空间的 1/8 大小。trimToSize 方法可以确保当前空间还有剩余。)
4、缓存的数据结构是什么呢?(LinkedHashMap<String,Map>)

LruMemoryCache:一种使用强引用来保存有数量限制的Bitmap的cache(在空间有限的情况,保留最近使用过的Bitmap)。每次Bitmap被访问时,它就被移动到一个队列的头部。当Bitmap被添加到一个空间已满的cache时,在队列末尾的Bitmap会被挤出去并变成适合被GC回收的状态。
注意:这个cache只使用强引用来保存Bitmap。

/** @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;
      //使用LinkedHashMap来缓存数据
        this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
    }

思考:为什么不用HashMap来缓存数据?好了,我们继续看源码

LruMemoryCache.get(…);

/**
     * 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) {
     //代码中除了异常判断,就是利用synchronized进行同步控制。
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        synchronized (this) {
            return map.get(key);
        }
    }
public V get(Object key) {
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }

LruMemoryCache保留在空间有限的情况下保留最近使用过的Bitmap。
LinkedHashMap中的get()方法不仅返回所匹配的值,并且在返回前还会将所匹配的key对应的entry调整在列表中的顺序(LinkedHashMap使用双链表来保存数据,不懂可以去看看Map集合类了解),让它处于列表的最后。当然,这种情况必须是在LinkedHashMap中accessOrder==true的情况下才生效的,反之就是get()方法不会改变被匹配的key对应的entry在列表中的位置。

@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)//调整entry在列表中的位置,其实就是双向链表的调整。它判断accessOrder
                 makeTail((LinkedEntry<K, V>) e);
             return e.value;
         }
 
         // Replace with Collections.secondaryHash when the VM is fast enough (http://b/8290590).
         int hash = 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;
     }

结论

LruMemoryCache缓存的存储数据结构是LinkedHashMap<String,Map>,在LinkedHashMap.get()方法执行后,LinkedHashMap中entry的顺序会得到调整。

如果图片过多,就会保留最近使用的,其他都要被回收,怎么才不被剔出呢?

需要定义的缓存空间是多大呢?
1、默认大小为 app 可用空间的 1/8 大小。
2、trimToSize 方法可以确保当前空间还有剩余。

LruMemoryCache中的trimToSize(…)这个函数就是用来限定LruMemoryCache的大小不要超过用户限定的大小,cache的大小由用户在LruMemoryCache刚开始初始化的时候限定。那么就看看LruMemoryCache中的put方法。

/** 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);
            //map.put()的返回值如果不为空,说明存在跟key对应的entry,put操作只是更新原有key对应的entry
            Bitmap previous = map.put(key, value);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
        }

        trimToSize(maxSize);
        return true;
    }

当Bitmap缓存的大小超过原来设定的maxSize时应该是在trimToSize(…)这个函数中做到的。遍历map,将多余的项(代码中对应toEvict)剔除掉,直到当前cache的大小等于或小于限定的大小。

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

磁盘缓存策略设计与实现

自己实现磁盘缓存,要考虑的太多,UIL提供了几种常见的磁盘缓存策略,当然你觉得都不符合你的要求,你也可以自己去扩展的。

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

在UIL中有着比较完整的存储策略,根据预先指定的空间大小,使用频率(生命周期),文件个数的约束条件,都有着对应的实现策略。最基础的接口DiscCacheAware和抽象类BaseDiscCache

UnlimitedDiscCache解析

UnlimitedDiscCache实现DiskCache接口,是ImageLoaderConfiguration中默认的磁盘缓存处理。用它的时候,磁盘缓存的大小是不受限的。
接下来看看实现UnlimitedDiscCache的源代码,通过源代码我们发现他其实就是继承了BaseDiscCache,这个类内部没有实现自己独特的方法,也没有重写什么,那么我们就直接看BaseDiscCache这个类。BaseDiscCache继承了DiscCache。在分析这个类之前,我们先想想自己实现一个磁盘缓存需要做多少麻烦的事情:

  1. 图片的命名会不会重。你没有办法知道用户下载的图片原始的文件名是怎么样的,因此很可能因为文件重名将有用的图片给覆盖掉了。
  2. 当应用卡顿或网络延迟的时候,同一张图片反复被下载。
  3. 处理图片写入磁盘可能遇到的延迟和同步问题。

直接上源码

/**
     * @param cacheDir          Directory for file caching
     * @param reserveCacheDir   null-ok; Reserve directory for file caching. It's used when the primary directory isn't available.
     * @param fileNameGenerator {@linkplain com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator
     *                          Name generator} for cached files
     */
    public BaseDiskCache(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;
    }

我们看一下BaseDiscCache的构造函数:
cacheDir: 文件缓存目录
reserveCacheDir: 备用的文件缓存目录,可以为null。它只有当cacheDir不能用的时候才有用。
fileNameGenerator: 文件名生成器。为缓存的文件生成文件名。

当看到fileNameGenerator,有点疑虑,我们并没有设置文件名啊?查阅代码发现是默认生成DefaultConfigurationFactory.createFileNameGenerator())。
代码如下:

/**
     * @param cacheDir        Directory for file caching
     * @param reserveCacheDir null-ok; Reserve directory for file caching. It's used when the primary directory isn't available.
     */
    public BaseDiskCache(File cacheDir, File reserveCacheDir) {
        this(cacheDir, reserveCacheDir, DefaultConfigurationFactory.createFileNameGenerator());
    }
/** Creates {@linkplain HashCodeFileNameGenerator default implementation} of FileNameGenerator */
    public static FileNameGenerator createFileNameGenerator() {
        return new HashCodeFileNameGenerator();
    }
/**
 * Names image file as image URI {@linkplain String#hashCode() hashcode}
 *
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
 * @since 1.3.1
 */
public class HashCodeFileNameGenerator implements FileNameGenerator {
    @Override
    public String generate(String imageUri) {
        return String.valueOf(imageUri.hashCode());
    }
}

UIL中有3种文件命名策略,默认的文件命名策略DefaultConfigurationFactory.createFileNameGenerator()。它是一个HashCodeFileNameGenerator。用String.hashCode()进行文件名的生成,这样也不会生成重复的文件名,就那么简单。

接着看看是如何存储数据的
直接看源码

@Override
    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
        //主要用于生成一个指向缓存目录中的文件,在这个函数里面调用了刚刚介绍过的fileNameGenerator来生成文件名。
        File imageFile = getFile(imageUri);
        //它是用来写入bitmap的临时文件,然后就把这个文件给删除了
        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 {
            if (loaded && !tmpFile.renameTo(imageFile)) {
                loaded = false;
            }
            if (!loaded) {
                tmpFile.delete();
            }
        }
        return loaded;
    }

UIL加载图片的一般流程是先判断内存中是否有对应的Bitmap,再判断磁盘(disk)中是否有,如果没有就从网络中加载。最后根据原先在UIL中的配置判断是否需要缓存Bitmap到内存或磁盘中。也就是说,当需要调用BaseDiscCache.save(…)之前,其实已经判断过这个文件不在磁盘中。

/** Returns file object (not null) for incoming image URI. File object can reference to non-existing file. */
    protected File getFile(String imageUri) {
       //利用fileNameGenerator生成一个唯一的文件名
        String fileName = fileNameGenerator.generate(imageUri);
        File dir = cacheDir;
        //当cacheDir不可用的时候,就是用reserveCachedir作为缓存目录了。
        if (!cacheDir.exists() && !cacheDir.mkdirs()) {
            if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
                dir = reserveCacheDir;
            }
        }
        //最后返回一个指向文件的对象,但是要注意当File类型的对象指向的文件不存在时,file会为null,而不是报错。
        return new File(dir, fileName);
    }

总结
内存缓存其实就是利用Map接口的对象在内存中进行缓存,可能有不同的存储机制。磁盘缓存其实就是将文件写入磁盘。这样就达到缓存目的。

    原文作者:安仔夏天勤奋
    原文地址: https://www.jianshu.com/p/d3bd2dbdbf9a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞