1. 程式人生 > >64.ImageLoader源代碼分析-磁盤命名和圖片緩存算法

64.ImageLoader源代碼分析-磁盤命名和圖片緩存算法

rusage ash ould isp 解釋 流程 imageload 打開 文件緩存

一. 前言

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源代碼分析-磁盤命名和圖片緩存算法