码迷,mamicode.com
首页 > 编程语言 > 详细

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

时间:2018-10-19 15:09:53      阅读:171      评论:0      收藏:0      [点我收藏+]

标签:工作流   ini   event   erro   create   逻辑   ges   就是   缓存   

一. 前言

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两个就是图片存储文件
技术分享图片

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

技术分享图片

  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 实现类

技术分享图片

下面详细看每个类的实现

六. 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<String, Entry> lruEntries =
         new LinkedHashMap<String, Entry>(0, 0.75f, true);

   ...
}

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

LinkedHashMap<String, Entry> 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 <b>true</b> - if copying should be continued; <b>false</b> - 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<File, Long> loadingDates = Collections.synchronizedMap(new HashMap<File, Long>());
@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,那么就删除它,从而实现了限制时间文件存储。

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

标签:工作流   ini   event   erro   create   逻辑   ges   就是   缓存   

原文地址:http://blog.51cto.com/483181/2306403

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!