文章目录

  • 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 文件结构

缓存的文件结构

在容器中模拟磁盘IO飙高的场景_ci

在容器中模拟磁盘IO飙高的场景_在容器中模拟磁盘IO飙高的场景_02