您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了“ImageLoader中如何實現磁盤命名和圖片緩存算法”,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“ImageLoader中如何實現磁盤命名和圖片緩存算法”這篇文章吧。
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產生一個圖片文件名稱的方法。
它包含兩個實現類
HashCodeFileNameGenerator
Md5FileNameGenerator
接下來,分別看這兩個類的實現
/** * 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命名。
/** * 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是操作記錄描述性文件,內容如下
DIRTY: 操作記錄創建,如果DIRTY后面沒有CLEAN或者REMOVE,那么這個圖片會被刪除。
CLEAN: 記錄成功創建和訪問
READ: 記錄成功訪問
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() | 清理所有的磁盤緩存 |
下面詳細看每個類的實現
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; }
首先根據imageUri生成文件名,也就是key,目前我們用的是hashCode
private String getKey(String imageUri) { return fileNameGenerator.generate(imageUri); }
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記錄。
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后綴。
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); }
IoUtils.closeSilently(os);
假設沒有出錯,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是抽象類,實現了基本的圖片文件存儲,獲取,刪除等操作,并沒有做什么限制。
如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)
和BaseDiskCache完全一樣,并沒有新的邏輯
限制存儲時間的文件存儲管理,當我們嘗試獲取緩存文件的時候會去刪除時間過長的文件,存儲的空間沒有限制。
我們以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,那么就刪除它,從而實現了限制時間文件存儲。
以上是“ImageLoader中如何實現磁盤命名和圖片緩存算法”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。