DiskLruCache是谷歌推荐的用来实现硬盘缓存的类,今天我们开始对于DiskLruCache的学习。DiskLruCache的测试代码: DiskLruCache的测试代码下载。关于FidkLruCache的使用,请参见我的博客:android基础---->LruCache的使用及原理
目录导航
DiskLruCache缓存的代码实例
我们通过一个案例来体会DiskLruCache的使用及执行的流程,在我们的项目中包含DiskLruCache文件:http://pan.baidu.com/s/1slR6pg5。项目结构如下:
一、 DiskLruCache的是一个final类,不能继承。如果我们要创建一个DiskLruCache的实例,则需要调用它的open()方法,如下:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
在AndroidManifest.xml加入网络权限和sd卡写入权限:
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
项目中创建并打开缓存:
try { File cacheDir = getDiskCacheDir(this, "bitmap"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(this), 1, 10 * 1024 * 1024); } catch (Exception e) { e.printStackTrace(); }
二、 从网络得到图片资源,并写入缓存:
private void saveCache() { new Thread(new Runnable() { @Override public void run() { try { String key = hashKeyForDisk(imageUrl); DiskLruCache.Editor editor = null; editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (downloadUrlToStream(imageUrl, outputStream)) { editor.commit(); } else { editor.abort(); } } //频繁的flush mDiskLruCache.flush(); handler.post(new Runnable() { @Override public void run() { textView.setText("saveCache done,the bitmap is ready"); } }); } catch (IOException e) { e.printStackTrace(); } } }).start(); }
downloadUrlToStream方法用于从网络上获取图片资源:
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) { HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024); out = new BufferedOutputStream(outputStream, 8 * 1024); int b; while ((b = in.read()) != -1) { out.write(b); } return true; } catch (final IOException e) { e.printStackTrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (final IOException e) { e.printStackTrace(); } } return false; }
三、 由于涉及到key的因素,我们写一个MD5对key进行编码:
public String hashKeyForDisk(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } private String bytesToHexString(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(); }
四、 从缓存中读取数据:
private void readCache() { //读取缓存 try { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(hashKeyForDisk(imageUrl)); if (snapshot != null) { InputStream is = snapshot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); imageView.setImageBitmap(bitmap); } else { imageView.setImageBitmap(null); } } catch (IOException e) { e.printStackTrace(); } }
五、 移除缓存:
private void removeCache() { try { mDiskLruCache.remove(hashKeyForDisk(imageUrl)); } catch (IOException e) { e.printStackTrace(); } }
六、 清空缓存:
private void deleteCache() { try { //delete()方法内部会调用close() mDiskLruCache.delete(); } catch (IOException e) { e.printStackTrace(); } }
七、 得到缓存的大小:
private void getCacheSize() { textView.setText("cache size : " + mDiskLruCache.size() + "B"); }
八、 在onDesctory方法中关闭缓存:
@Override protected void onDestroy() { super.onDestroy(); try { //关闭DiskLruCache,与open对应 mDiskLruCache.close(); } catch (IOException e) { e.printStackTrace(); } }
DiskLruCache的原理分析
经过上述实例的说明,我们对DiskLruCache的使用已经有了一些认识。现在我们开始DiskLruCache的原理分析:
创建打开缓存
一、 当创建打开缓存:mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(this), 1, 10 * 1024 * 1024);
创建日志journal文件,并且初始化journalWriter:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } if (valueCount <= 0) { throw new IllegalArgumentException("valueCount <= 0"); } // 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) { cache.delete(); } } // create a new empty cache directory.mkdirs(); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); cache.rebuildJournal(); return cache; }
在DiskLruCache的构造方法中,创建journal和journal.tmp文件。
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { this.directory = directory; this.appVersion = appVersion; this.journalFile = new File(directory, JOURNAL_FILE); this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); this.valueCount = valueCount; this.maxSize = maxSize; }
在rebuildJournal方法中,在journalFileTmp临时文件中,写入一些数据,最后重命名为journalFile:
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); }
二、 创建完成之后 ,在sd中存在journal文件:内容如下:
libcore.io.DiskLruCache // MAGIC 1 // VERSION_1 1 // appVersion 1 // valueCount
写入缓存
二、 然后是这个代码:DiskLruCache.Editor editor = mDiskLruCache.edit(key);其中的lruEntries是一个LinkedHashMap,用于记录资源的key与value。
当第一次edit时,lruEntries.get(key)返回的是空。这里会创建一个Entry对象,并在日志文件中写入DIRTY + ' ' + key + '\n'内容。最后返回包含这个entry的Editor。
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; }
三、 OutputStream outputStream = editor.newOutputStream(0);downloadUrlToStream(imageUrl, outputStream);得到文件输出流,将从网络上请求到的资源写入到文件中。
这里关注下outputStream,它是由newOutputStream方法得到的:
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))); } }
getDirtyFile文件是一个临时文件,也就是网络文件写入到这个临时文件当中:
public File getDirtyFile(int i) { return new File(directory, key + "." + i + ".tmp"); }
四、 接着执行到了editor.commit()方法,如果没有出现错误的话:
创建一个key + "." + i的文件,将上述的dirty文件重命名为key + "." + i的文件。更新已经缓存的大小,并且删除上述的dirty文件。
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) { 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); } }
五、 当执行完写入之后,日志文件如下内容:abb7d9d7add6b9fba5314aec6e60c9e6就是上述MD5生成的key,15417是代表缓存的大小。并生成一个abb7d9d7add6b9fba5314aec6e60c9e6.0文件.
libcore.io.DiskLruCache 1 1 1 DIRTY abb7d9d7add6b9fba5314aec6e60c9e6 CLEAN abb7d9d7add6b9fba5314aec6e60c9e6 15417
从缓存中读取
一、 mDiskLruCache.get(key,以下是关键代码,中间省略了代码);
Entry entry = lruEntries.get(key); // 根据key从map中得到相应的value .... InputStream[] ins = new InputStream[valueCount]; try { for (int i = 0; i < valueCount; i++) { ins[i] = new FileInputStream(entry.getCleanFile(i)); // 得到clearn文件输入流 } } catch (FileNotFoundException e) { // a file must have been deleted manually! return null; } journalWriter.append(READ + ' ' + key + '\n'); // 在日志文件中写入内容 .... return new Snapshot(key, entry.sequenceNumber, ins); // 返回一个Snapshot对象
二、 snapshot.getInputStream(0)得到clean文件的输入流,然后通过Bitmap bitmap = BitmapFactory.decodeStream(is);方法得到缓存在硬盘的图片。