一. 前言

ImageLoader的图片缓存分成磁盘和内存两种,这里分析一下磁盘缓存以及图片文件名算法的实现

默认是不存储在磁盘上的,需要手动打开开关

如下

DisplayImageOptions options = new DisplayImageOptions.Builder()
                .cacheInMemory(true) // default false
                .cacheOnDisk(true) // default false

imageLoader.displayImage("", imageView, options, null, null);

二. 磁盘文件命名

/**
 * Generates names for files at disk cache
 *
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
 * @since 1.3.1
 */
public interface FileNameGenerator {

   /** Generates unique file name for image defined by URI */
   String generate(String imageUri);
}

接口是FileNameGenerator,此接口非常简单明了,只有一个根据图片uri产生一个图片文件名称的方法。

它包含两个实现类

  1. HashCodeFileNameGenerator
  2. Md5FileNameGenerator

接下来,分别看这两个类的实现

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

实现比较简单,根据uri的hashcode转化成String即可,默认就是Hashcode命名。

2.2 Md5FileNameGenerator

/**
 * Names image file as MD5 hash of image URI
 *
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
 * @since 1.4.0
 */
public class Md5FileNameGenerator implements FileNameGenerator {

   private static final String HASH_ALGORITHM = "MD5";
   private static final int RADIX = 10 + 26; // 10 digits + 26 letters

   @Override
   public String generate(String imageUri) {
      byte[] md5 = getMD5(imageUri.getBytes());
      BigInteger bi = new BigInteger(md5).abs();
      return bi.toString(RADIX);
   }

   private byte[] getMD5(byte[] data) {
      byte[] hash = null;
      try {
         MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM);
         digest.update(data);
         hash = digest.digest();
      } catch (NoSuchAlgorithmException e) {
         L.e(e);
      }
      return hash;
   }
}

通过imageUri得到byte数组,然后通过MD5算法得到文件名

三. 磁盘目录选择

一般默认优先选择sdk/android/data/packageName/cache/uil-images卡,如果sdk目录创建失败,那么会选择/data/data/packageName目录

四. 图片缓存示例

其中-1557665659.0和1238391484.0两个就是图片存储文件
64.ImageLoader源代码分析-磁盘命名和图片缓存算法_第1张图片

journal是操作记录描述性文件,内容如下

64.ImageLoader源代码分析-磁盘命名和图片缓存算法_第2张图片

  1. DIRTY: 操作记录创建,如果DIRTY后面没有CLEAN或者REMOVE,那么这个图片会被删除。
  2. CLEAN: 记录成功创建和访问
  3. READ: 记录成功访问
  4. REMOVE: 记录删除

五. 磁盘缓存接口

磁盘缓存算法的接口是DiskCache,接口很简单明了。

public interface DiskCache {

   File getDirectory();

   File get(String imageUri);

   boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;

   boolean save(String imageUri, Bitmap bitmap) throws IOException;

   boolean remove(String imageUri);

   void close();

   void clear();
}
方法名 解释
getDirectory() 获取存储目录
get(String imageUri) 根据imageUri获取图片文件
save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) 保存图片
remove(String imageUri) 删除图片缓存
close() 关闭磁盘缓存,释放资源
clear() 清理所有的磁盘缓存

5.1 实现类

64.ImageLoader源代码分析-磁盘命名和图片缓存算法_第3张图片

下面详细看每个类的实现

六. LruDiskCache

public class LruDiskCache implements DiskCache {
    protected DiskLruCache cache;
    ...
    protected final FileNameGenerator fileNameGenerator;
    ...

    public LruDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize,
            int cacheMaxFileCount) throws IOException {
        ...
        initCache(cacheDir, reserveCacheDir, cacheMaxSize, cacheMaxFileCount);
    }

    private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
            throws IOException {
        try {
            cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
        } catch (IOException e) {
            ...
        }
    }

    @Override
    public File getDirectory() {
        return cache.getDirectory();
    }

    @Override
    public File get(String imageUri) {
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = cache.get(getKey(imageUri));
            return snapshot == null ? null : snapshot.getFile(0);
        } catch (IOException e) {
            L.e(e);
            return null;
        } finally {
            if (snapshot != null) {
                snapshot.close();
            }
        }
    }

    @Override
    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
        DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
        if (editor == null) {
            return false;
        }

        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
        boolean copied = false;
        try {
            copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);
        } finally {
            IoUtils.closeSilently(os);
            if (copied) {
                editor.commit();
            } else {
                editor.abort();
            }
        }
        return copied;
    }
    ...

    @Override
    public boolean remove(String imageUri) {
        try {
            return cache.remove(getKey(imageUri));
        } catch (IOException e) {
            L.e(e);
            return false;
        }
    }

    @Override
    public void close() {
        try {
            cache.close();
        } catch (IOException e) {
            L.e(e);
        }
        cache = null;
    }

    @Override
    public void clear() {
        try {
            cache.delete();
        } catch (IOException e) {
            L.e(e);
        }
        try {
            initCache(cache.getDirectory(), reserveCacheDir, cache.getMaxSize(), cache.getMaxFileCount());
        } catch (IOException e) {
            L.e(e);
        }
    }

    private String getKey(String imageUri) {
        return fileNameGenerator.generate(imageUri);
    }
}

LruDiskCache有几个比较重要的属性,

protected DiskLruCache cache;
protected final FileNameGenerator fileNameGenerator;

FileNameGenerator就是上面说的文件命名生成器,包括hashcode和md5算法。我们思考下,为什么需要FileNameGenerator?

个人以为网络上面的uri可能是千奇百怪的,甚至包括特殊字符,那作为文件名显然不合适。所以,这个时候来一次hashcode,或者md5转换,获取文件名是最好的。

DiskLruCache,窃以为这个命名不是很好,因为跟LruDiskCache很类似(我第一眼就看成一个东西了!)

这个DiskLruCache很重要,它维护了磁盘图片文件缓存的操作记录,缓存和文件对应关系等。

而且如果你仔细看LruDiskCache的各个方法时会发现,基本都是调用cache的对应方法。

所以,我们主要接下来看DiskLruCache代码

final class DiskLruCache implements Closeable {
   ...
   private final File directory;
   private final File journalFile;
   ...
   private Writer journalWriter;
   private final LinkedHashMap lruEntries =
         new LinkedHashMap(0, 0.75f, true);

   ...
}

DiskLruCache包含了journalFile,文件里面具体的含义可以第四点的样例。包含了

LinkedHashMap lruEntries 

表示每个图片的缓存记录,String表示key, Entry表示图片的描述信息

private final class Entry {
   private final String key;

   /** Lengths of this entry's files. */
   private final long[] lengths;

   /** True if this entry has ever been published. */
   private boolean readable;

   /** The ongoing edit or null if this entry is not being edited. */
   private Editor currentEditor;

   /** The sequence number of the most recently committed edit to this entry. */
   private long sequenceNumber;

   public File getCleanFile(int i) {
      return new File(directory, key + "." + i);
   }

   public File getDirtyFile(int i) {
      return new File(directory, key + "." + i + ".tmp");
   }
}

我们以保存图片缓存为例,分析下LruDiskCache的工作流程,首先看LruDiskCache的save方法

public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
   DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
   if (editor == null) {
      return false;
   }

   OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
   boolean copied = false;
   try {
      copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);
   } finally {
      IoUtils.closeSilently(os);
      if (copied) {
         editor.commit();
      } else {
         editor.abort();
      }
   }
   return copied;
}

6.1 getkey(imageUri)

首先根据imageUri生成文件名,也就是key,目前我们用的是hashCode

private String getKey(String imageUri) {
   return fileNameGenerator.generate(imageUri);
}

6.2 cache.edit

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
   checkNotClosed();
   validateKey(key);
   Entry entry = lruEntries.get(key);
   if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
         || entry.sequenceNumber != expectedSequenceNumber)) {
      return null; // Snapshot is stale.
   }
   if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
   } else if (entry.currentEditor != null) {
      return null; // Another edit is in progress.
   }

   Editor editor = new Editor(entry);
   entry.currentEditor = editor;

   // Flush the journal before creating files to prevent file leaks.
   journalWriter.write(DIRTY + ' ' + key + '\n');
   journalWriter.flush();
   return editor;
}

从lruEntries里面根据key获取到对应的图片Entry对象,如果没有就新建一个。

然后利用journalWriter写入一条DIRTY记录。

6.3 DiskLruCache 打开Dirty图片文件流

public OutputStream newOutputStream(int index) throws IOException {
   synchronized (DiskLruCache.this) {
      if (entry.currentEditor != this) {
         throw new IllegalStateException();
      }
      if (!entry.readable) {
         written[index] = true;
      }
      File dirtyFile = entry.getDirtyFile(index);
      FileOutputStream outputStream;
      try {
         outputStream = new FileOutputStream(dirtyFile);
      } catch (FileNotFoundException e) {
         // Attempt to recreate the cache directory.
         directory.mkdirs();
         try {
            outputStream = new FileOutputStream(dirtyFile);
         } catch (FileNotFoundException e2) {
            // We are unable to recover. Silently eat the writes.
            return NULL_OUTPUT_STREAM;
         }
      }
      return new FaultHidingOutputStream(outputStream);
   }
}
public File getDirtyFile(int i) {
   return new File(directory, key + "." + i + ".tmp");
}

注意这里打开的是drity文件,就是正常的文件后面加上一个.tmp后缀。

6.4 copyStream把网络图片流写入Dirty文件

public static boolean copyStream(InputStream is, OutputStream os, CopyListener listener, int bufferSize)
      throws IOException {
   int current = 0;
   int total = is.available();
   if (total <= 0) {
      total = DEFAULT_IMAGE_TOTAL_SIZE;
   }

   final byte[] bytes = new byte[bufferSize];
   int count;
   if (shouldStopLoading(listener, current, total)) return false;
   while ((count = is.read(bytes, 0, bufferSize)) != -1) {
      os.write(bytes, 0, count);
      current += count;
      if (shouldStopLoading(listener, current, total)) return false;
   }
   os.flush();
   return true;
}
private static boolean shouldStopLoading(CopyListener listener, int current, int total) {
   if (listener != null) {
      boolean shouldContinue = listener.onBytesCopied(current, total);
      if (!shouldContinue) {
         if (100 * current / total < CONTINUE_LOADING_PERCENTAGE) {
            return true; // if loaded more than 75% then continue loading anyway
         }
      }
   }
   return false;
}

很普通的文件流读写,有意思的是shouldStopLoading,它给了我们一个使用listener终止copy的时机。

public static interface CopyListener {
   /**
    * @param current Loaded bytes
    * @param total   Total bytes for loading
    * @return true - if copying should be continued; false - if copying should be interrupted
    */
   boolean onBytesCopied(int current, int total);
}

6.5 关闭Dirty文件流

IoUtils.closeSilently(os);

6.6 写入图片文件

假设没有出错,completeEdit里面,会把dirty文件正式名称成图片缓存文件

dirty.renameTo(clean);

然后写入一条CLEAN或者REMOVE操作日志到journal文件中。

具体可以看代码

editor.commit();
public void commit() throws IOException {
   if (hasErrors) {
      completeEdit(this, false);
      remove(entry.key); // The previous entry is stale.
   } else {
      completeEdit(this, true);
   }
   committed = true;
}
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
   ...

   for (int i = 0; i < valueCount; i++) {
      File dirty = entry.getDirtyFile(i);
      if (success) {
         if (dirty.exists()) {
            File clean = entry.getCleanFile(i);
            dirty.renameTo(clean); //保存dirty到正式图片文件
            long oldLength = entry.lengths[i];
            long newLength = clean.length();
            entry.lengths[i] = newLength;
            size = size - oldLength + newLength;
            fileCount++;
         }
      } else {
         deleteIfExists(dirty);
      }
   }

   redundantOpCount++;
   entry.currentEditor = null;
   if (entry.readable | success) {// 写入CLEAN操作日志
      entry.readable = true;
      journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
      if (success) {
         entry.sequenceNumber = nextSequenceNumber++;
      }
   } else {
      lruEntries.remove(entry.key); //操作失败,写入REMOVE操作日志
      journalWriter.write(REMOVE + ' ' + entry.key + '\n');
   }
   journalWriter.flush();

   if (size > maxSize || fileCount > maxFileCount || journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
   }
}

这样一次文件保存操作就完成了。

七. BaseDiskCache

BaseDiskCache是抽象类,实现了基本的图片文件存储,获取,删除等操作,并没有做什么限制。

如save和get, remove等操作

public abstract class BaseDiskCache implements DiskCache {
   ...

   protected final FileNameGenerator fileNameGenerator;
   ...

   @Override
   public File getDirectory() {
      return cacheDir;
   }

   @Override
   public File get(String imageUri) {
      return getFile(imageUri);
   }

   @Override
   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 {
         if (loaded && !tmpFile.renameTo(imageFile)) {
            loaded = false;
         }
         if (!loaded) {
            tmpFile.delete();
         }
      }
      return loaded;
   }

   @Override
   public boolean remove(String imageUri) {
      return getFile(imageUri).delete();
   }

   @Override
   public void close() {
      // Nothing to do
   }

   @Override
   public void clear() {
      File[] files = cacheDir.listFiles();
      if (files != null) {
         for (File f : files) {
            f.delete();
         }
      }
   }

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

以save为例,首先会生成一个tmp文件,然后把网络图片文件流写入tmp文件。

OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile),
loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);

然后把tmp文件重新名称成正式的文件

tmpFile.renameTo(imageFile)

八. UnlimitedDiskCache

和BaseDiskCache完全一样,并没有新的逻辑

九. LimitedAgeDiskCache

限制存储时间的文件存储管理,当我们尝试获取缓存文件的时候会去删除时间过长的文件,存储的空间没有限制。

我们以save和get为例

private final Map loadingDates = Collections.synchronizedMap(new HashMap());
@Override
public boolean save(String imageUri, Bitmap bitmap) throws IOException {
   boolean saved = super.save(imageUri, bitmap);
   rememberUsage(imageUri);
   return saved;
}
private void rememberUsage(String imageUri) {
   File file = getFile(imageUri);
   long currentTime = System.currentTimeMillis();
   file.setLastModified(currentTime);
   loadingDates.put(file, currentTime);
}

save的时候,会调用rememberUsage方法,使用一个HashMap来存储缓存时间。

get

@Override
public File get(String imageUri) {
   File file = super.get(imageUri);
   if (file != null && file.exists()) {
      boolean cached;
      Long loadingDate = loadingDates.get(file);
      if (loadingDate == null) {
         cached = false;
         loadingDate = file.lastModified();
      } else {
         cached = true;
      }

      if (System.currentTimeMillis() - loadingDate > maxFileAge) {
         file.delete();
         loadingDates.remove(file);
      } else if (!cached) {
         loadingDates.put(file, loadingDate);
      }
   }
   return file;
}

get的时候会根据当前时间和缓存时间比较,如果大于maxFileAge,那么就删除它,从而实现了限制时间文件存储。