首先,摆一套基本的使用流程
/**
* 初始化
*/
private void initDisk() {
File cacheDir = getDiskCacheDir(MainActivity.this, "bitmap");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
try {
diskLruCache = DiskLruCache.open(cacheDir, getAppVersion(MainActivity.this), 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 缓存到磁盘
*/
private void diskCache() {
new Thread(new Runnable() {
@Override
public void run() {
try {
String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKey(imageUrl);
DiskLruCache.Editor editor = diskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (download(imageUrl, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
diskLruCache.flush();
} catch (Exception e) {
}
}
}).start();
}
/**
* 读取缓存
*/
private void readCache() {
String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKey(imageUrl);
try {
DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
if (snapshot != null) {
InputStream is = snapshot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
imageView.setImageBitmap(bitmap);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 移除缓存
*/
private void removeCache() {
String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKey(imageUrl);
try {
diskLruCache.remove(key);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取缓存路径
* @param context
* @param name
* @return
*/
public File getDiskCacheDir(Context context, String name) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
Log.e(TAG, "path:"+cachePath);
return new File(cachePath + File.separator + name);
}
/**
* 获取版本路径
* @param context
* @return
*/
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(),0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
public String hashKey(String key) {
String cacheKey="";
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(key.getBytes());
cacheKey = bytesToHex(digest.digest());
} catch (Exception e) {
e.printStackTrace();
}
return cacheKey;
}
public String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xff & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/**
* 下载
* @param urlStr
* @param outputStream
* @return
*/
private boolean download(String urlStr, OutputStream outputStream) {
HttpURLConnection connection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
URL url = new URL(urlStr);
connection= (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(connection.getInputStream(), 8 * 1024);
out = new BufferedOutputStream(outputStream, 8*1024);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (Exception e){
}finally {
if (connection != null) {
connection.disconnect();
}
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return false;
}
在sd卡可用的情况下,缓存路径是/storage/emulated/0/Android/data/kdjz.mobile.qeeniao.com.cachedemo/cache
我们找到该路径,会发现一堆名字很长的文件和一个journal文件,前者是缓存的图片文件,后者是各种操作的记录
接下来,仔细分析流程,首先看open方法
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
// prefer to pick up where we left off
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
IO_BUFFER_SIZE);
return cache;
} catch (IOException journalIsCorrupt) {
// System.logW("DiskLruCache " + directory + " is corrupt: "
// + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
// create a new empty cache
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
一开始没有journal文件,会通过cache.rebuildJournal();生成
static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_TMP = "journal.tmp";
static final String MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = "1";
private synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
}
Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
writer.write(MAGIC);
writer.write("\n");
writer.write(VERSION_1);
writer.write("\n");
writer.write(Integer.toString(appVersion));
writer.write("\n");
writer.write(Integer.toString(valueCount));
writer.write("\n");
writer.write("\n");
for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) {
writer.write(DIRTY + ' ' + entry.key + '\n');
} else {
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
}
}
writer.close();
journalFileTmp.renameTo(journalFile);
journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
}
这也就是journal前四行的格式,MAGIC、VERSION_1、appVersion和valueCount,目前lruEntries为空,还没有具体的数据
这里先将数据写入到文件journalFileTmp中,然后再重命名为journalFile,journalWriter用于向文件写入数据
在完成初始化后,下一步就可以写入数据了
String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKey(imageUrl);
DiskLruCache.Editor editor = diskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (download(imageUrl, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
diskLruCache.flush();
先要获取Editor
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
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;
}
先以key创建了Entry,然后又创建了Editor,然后写入了DIRTY key,再获取editor后,通过他获取文件输出流
//这里传进来的index为0
public OutputStream newOutputStream(int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
}
}
public File getDirtyFile(int i) {
return new File(directory, key + "." + i + ".tmp");
}
这里图片文件的名称格式为 key + “.” + i + “.tmp”
可以看出,下载并写入成功后,会调用editor.commit(),否则会调用editor.abort(),一个个看
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // the previous entry is stale
} else {
completeEdit(this, true);
}
}
这里的hasErrors什么时候为true呢,我们这里的文件输出流类是FaultHidingOutputStream
private class FaultHidingOutputStream extends FilterOutputStream {
private FaultHidingOutputStream(OutputStream out) {
super(out);
}
@Override public void write(int oneByte) {
try {
out.write(oneByte);
} catch (IOException e) {
hasErrors = true;
}
}
@Override public void write(byte[] buffer, int offset, int length) {
try {
out.write(buffer, offset, length);
} catch (IOException e) {
hasErrors = true;
}
}
@Override public void close() {
try {
out.close();
} catch (IOException e) {
hasErrors = true;
}
}
@Override public void flush() {
try {
out.flush();
} catch (IOException e) {
hasErrors = true;
}
}
}
所以,如果在写入过程中出现异常,hasErrors就会为true,所以正常会走completeEdit
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
// if this edit is creating the entry for the first time, every index must have a value
if (success && !entry.readable) { //如果没有对应的dirty文件,就直接调用editor.abort()
for (int i = 0; i < valueCount; i++) {
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
throw new IllegalStateException("edit didn't create file " + i);
}
}
}
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);
long oldLength = entry.lengths[i];
long newLength = clean.length();
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
deleteIfExists(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
public File getCleanFile(int i) {
return new File(directory, key + "." + i);
}
clean文件和dirty文件的区别就是少了.tmp后缀,如果成功了,会将dirty文件转换成clean文件,并且更新size大小
然后,更新一些参数的值,并且写入一条clean记录,clean记录后面加上了文件的大小,可以用于统计缓存大小
缓存完文件后,就可以进行读取了
private void readCache() {
String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKey(imageUrl);
try {
DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
if (snapshot != null) {
InputStream is = snapshot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
imageView.setImageBitmap(bitmap);
}
} catch (Exception e) {
e.printStackTrace();
}
}
首先,获取到Snapshot
public synchronized Snapshot get(String key) throws IOException {
Entry entry = lruEntries.get(key);
if (!entry.readable) {
return null;
}
/*
* Open all streams eagerly to guarantee that we see a single published
* snapshot. If we opened streams lazily then the streams could come
* from different edits.
*/
InputStream[] ins = new InputStream[valueCount];
try {
for (int i = 0; i < valueCount; i++) {
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
} catch (FileNotFoundException e) {
// a file must have been deleted manually!
return null;
}
redundantOpCount++;
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins);
}
通过cleanFile获取输入流,然后往journal文件中写入一条read记录,snapshot是一个封装
那么后续snapshot.getInputStream(0)就可以获取到对应文件的流,然后设置到Imageview了
要记住一点,只有在调用flush之后,记录才会写到journal里,否则只是保存在缓存中
我们记得最开始是没有数据情况下的初始化,那么现在有数据了,初始化流程会有什么变化呢?
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
IO_BUFFER_SIZE);
return cache;
}
}
首先是readJournal
private void readJournal() throws IOException {
InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
try {
...一些验证...
while (true) {
try {
readJournalLine(readAsciiLine(in));
} catch (EOFException endOfJournal) {
break;
}
}
} finally {
closeQuietly(in);
}
}
private void readJournalLine(String line) throws IOException {
String[] parts = line.split(" ");
if (parts.length < 2) {
throw new IOException("unexpected journal line: " + line);
}
String key = parts[1];
if (parts[0].equals(REMOVE) && parts.length == 2) {
lruEntries.remove(key);
return;
}
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(copyOfRange(parts, 2, parts.length));
} else if (parts[0].equals(DIRTY) && parts.length == 2) {
entry.currentEditor = new Editor(entry);
} else if (parts[0].equals(READ) && parts.length == 2) {
// this work was already done by calling lruEntries.get()
} else {
throw new IOException("unexpected journal line: " + line);
}
}
这里通过journal文件的记录,重新生成数据保存在lruEntries中,然后是processJournal
/**
* Computes the initial size and collects garbage as a part of opening the
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
*/
private void processJournal() throws IOException {
deleteIfExists(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}
通过entry.currentEditor是否为空来判断数据是clean还是dirty,clean的话会计算size,否则会清空数据。
然后就生成journalWriter,后续的流程就没有什么差别了。
使用流程基本分析完毕,那么当缓存的数据越来越多,是如何去清理的呢?
其实我们在每一步操作时,都会将操作数redundantOpCount++,然后会调用一些清理的判断条件,先以read为例
redundantOpCount++;
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
private boolean journalRebuildRequired() {
final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
&& redundantOpCount >= lruEntries.size();
}
可以看出,操作次数有2000的限制,remove和read情况相同,而save则多了一项判断
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
这是因为只有save才会使得size变大,所以需要控制范围
private final Callable<Void> cleanupCallable = new Callable<Void>() {
@Override public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // closed
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
在trimToSize里,则是对多余的数据进行删除
private void trimToSize() throws IOException {
while (size > maxSize) {
// Map.Entry<String, Entry> toEvict = lruEntries.eldest();
final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
很简单,通过while循环,只要size还大于maxSize,则进行删除,大家可能会说,这说的也太含糊了。哈哈,其实这里的lruEntries是一个LinkedHashMap,
这里的具体实现是
private final LinkedHashMap<String, Entry> lruEntries
= new LinkedHashMap<String, Entry>(0, 0.75f, true);
最后一个参数如果为false,map中的数据会按照插入流程排序,先插入的先出,如果为true,则按照访问的顺序排序,最近最少访问的数据会先出。(内存缓存LruCache也是利用的这一原理)
因此使用的较多的数据缓存时间更长,到此,关于DiskLruCache的分析也就结束了。