文章目录
- 1 初始化
- 1.1 DiskLruCache的创建
- 1.2 读取journalFile
- 1.3 给lruEntries赋值
- 2 添加过程
- 2.1 添加调用
- 2.2 获取Eidtor对象
- 3.2 将value数据写入到本地
- 3 读取过程
- 3.1 获取SnapShot对象
- 3.2 通过SnapShot读取文件
- 4 总结
- 5 文件结构
1 初始化
1.1 DiskLruCache的创建
DiskLruCache.open(cacheDir, 1, 1, 1024 * 1024 * 10);
通过静态方法open()来创建DiskLruCache对象
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
//1 创建DiskLruCache对象
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
//2 将journalFile中的数据读取到lruEntries中
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;
}
内部会调用DiskLruCache的构造方法
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
this.directory = directory; //journalFile的所在的文件夹路径
this.appVersion = appVersion; //缓存版本
this.journalFile = new File(directory, JOURNAL_FILE);//jounal文件
this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);//用于保证journalFile的完成性
this.valueCount = valueCount; //一个可以对应几个value
this.maxSize = maxSize; //最大缓存大小
}
journalFileTmp的用处
journalFileTmp.renameTo(journalFile);
1.2 读取journalFile
private void readJournal() throws IOException {
InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
try {
String magic = readAsciiLine(in); //读取cache文件标识 --> libcore.io.DiskLruCache
String version = readAsciiLine(in);//读取文件版本
String appVersionString = readAsciiLine(in);//读取应用版本
String valueCountString = readAsciiLine(in);//读取最大缓存数量
String blank = readAsciiLine(in); //换行
if (!MAGIC.equals(magic)//读取正文
|| !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString)
|| !"".equals(blank)) {
throw new IOException("unexpected journal header: ["
+ magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
}
//遍历读取正文
while (true) {
try {
readJournalLine(readAsciiLine(in));
} catch (EOFException endOfJournal) {
break;
}
}
} finally {
closeQuietly(in);
}
}
1.3 给lruEntries赋值
lruEntries的类型是
LinkedHashMap<String, Entry> lruEntries
private void readJournalLine(String line) throws IOException {
//1 以parts标识这一行的内容被空格分隔开后的字符串数组
String[] parts = line.split(" ");
//以 CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 为例
//parts[0] = CLEAN
//parts[1] = 3400330d1dfc7f3f7f4b8d4d803dfcf6
String key = parts[1];
//2 如果是remove标识 移除这一项
if (parts[0].equals(REMOVE) && parts.length == 2) {
lruEntries.remove(key);
return;
}
//3 创建key在lruEntries中对应的value
Entry entry = lruEntries.get(key);
if (entry == null) {
// 4 以key创建Entry
entry = new Entry(key);
//5 将entriy添加到lruEntries中
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)); //设置key的长度
} 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);
}
}
经过while循环后,此时已经把journalFile中的内容全部读取到lruEntries中了
lruEntries是以key为键,Entry为值Map,并维护了双向链表来保证每个键值对的顺序。但是注意此时并没有把key对应的实际的value读取到内存中,而是通过以Entry的形式来保存。后续也不会保存到内容中,但Client真正需要拿到value时,是以InputStream的方式去本地读取。
2 添加过程
2.1 添加调用
这里要注意,后续的DiskLruCache要统一封装为单例对象,避免全局多个DiskLruCache导致的异步问题。
private void addDiskCache(String key, String value) throws IOException {
String filePath = getCacheDir() + "/cache";
File cacheDir = new File(filePath);
//1 获取DiskLruCache对象
DiskLruCache diskLruCache = DiskLruCache.open(cacheDir, 1, 1, 1024 * 1024 * 10);
//2 获取Editor对象
DiskLruCache.Editor editor = diskLruCache.edit(key);
//3 将value数据写入到本地
editor.newOutputStream(0).write(value.getBytes());
//4 提交此次编辑内容
editor.commit();
//5 关闭流
diskLruCache.close();
}
2.2 获取Eidtor对象
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) {
//1 创建key对应的Entry对象
entry = new Entry(key);
//2 将key value添加到LruEntries中
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // another edit is in progress
}
//3 用entry创建Editor对象
Editor editor = new Editor(entry);
//4 将Editor对象赋值给entry
entry.currentEditor = editor;
//5 每次获取Edit对象都会触发写入一行DIRTY类型的标记
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
3.2 将value数据写入到本地
Editor#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)));
}
}
Entry#getDirtyFile()
public File getDirtyFile(int i) {
return new File(directory, key + "." + i + ".tmp");
}
3 读取过程
DisLruCache通过key来获取对应的value方式:
String value = diskLruCache.get(key).getString(0);
3.1 获取SnapShot对象
DisLruCache#get(key)
public synchronized Snapshot get(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
if (!entry.readable) {
return null;
}
//1 创建输入流数组
InputStream[] ins = new InputStream[valueCount];
try {
for (int i = 0; i < valueCount; i++) {
//2 key对应的实际数据的路径创建FileInputStream 此处的key表示一个key对应多个缓存文件
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
} catch (FileNotFoundException e) {
// a file must have been deleted manually!
return null;
}
redundantOpCount++;
// 3 每次读取都对应一次READ标记
journalWriter.append(READ + ' ' + key + '\n');
//这块重创建没看懂
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
//4 创建SnapShot并返回
return new Snapshot(key, entry.sequenceNumber, ins);
}
这里的getCleanFile()即为key对应的实际数据存储的路径
public File getCleanFile(int i) {
return new File(directory, key + "." + i);
}
3.2 通过SnapShot读取文件
Snapshot#getString()
public String getString(int index) throws IOException {
return inputStreamToString(getInputStream(index));
}
private static String inputStreamToString(InputStream in) throws IOException {
return readFully(new InputStreamReader(in, UTF_8));
}
public static String readFully(Reader reader) throws IOException {
try {
StringWriter writer = new StringWriter();
char[] buffer = new char[1024];
int count;
while ((count = reader.read(buffer)) != -1) {
writer.write(buffer, 0, count);
}
return writer.toString();
} finally {
reader.close();
}
}
4 总结
- DiskLruCache通过LinkedHashMap和本地的journal文件来实现文件的本地数据的存储和获取
- LinkHashMap是有序的散列链表,在访问或者插入数据时,会把数据插入到有序链表的尾部,来实现最近最少访问的策略
- jounal文件主要保存了操作的类型,保存数据对应的key,和value的大小,每一次的REMOVE和CLEAN之前都会对应一个DIRTY
- 同时在jounal所在的文件路径下,会有以jounal文件中的key来命名的文件,此文件即为key对应的value
- DiskLruCache初始化时,会首先读取jounal文件并将数据结构转化并赋值给LinkedHashMap对象
- LinkedHashMap对象保存的value实际上时Entry对象,真正的value只有在get()方法触发时才会从本地文件中读取
5 文件结构
缓存的文件结构