一. 前言
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
接下来,分别看这两个类的实现
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是操作记录描述性文件,内容如下
- 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() | 清理所有的磁盘缓存 |
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 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,那么就删除它,从而实现了限制时间文件存储。