文章目录

  • 一、前言
  • 二、基本使用
    • 2.1 打开缓存
    • 2.2 写入缓存
    • 2.3 读取缓存
    • 2.4 移除缓存
  • 三、journal 日志格式
  • 四、源码解析
    • 4.1 DiskLruCache.open()
    • 4.2 DiskLruCache.edit(key)
    • 4.3 DiskLruCache.get(key)
    • 4.4 DiskLruCache.remove(key)
    • 4.4 DiskLruCache.rebuildJournal()
  • 五、总结

一、前言

我们知道 Glide 图片框架的内存缓存用的是 LruCache,磁盘缓存是 DiskLruCache,它们用的都是LRU算法。内存缓存的 LRU 还比较好理解,那么文件是如何按照 LRU 的思想进行管理的呢?本文将以 Glide 框架内的 DiskLruCache 代码来分析它的实现方式。

DiskLruCache 资源:

  • JakeWharton 大佬的 DiskLruCache
  • Glide:implementation 'com.github.bumptech.glide:glide:4.11.0'

二、基本使用

下面将介绍下面4种基本操作。

  • 打开缓存
  • 写入缓存
  • 读取缓存
  • 移除缓存

2.1 打开缓存

DiskLruCache 对象只能通过 open() 这个静态方法构建。

/*** directory:数据的缓存地址* APP_VERSION:当前应用程序的版本号* VALUE_COUNT:指定每个缓存的key可以对应多少个文件,基本都传1.* maxSize:允许缓存的最大容量,单位:字节*/
DiskLruCache lruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);

2.2 写入缓存

缓存的写入需要借助 Editor 对象来实现,与 SharePreferences 类似。且需要对外提供一个文件对象 (需要调用方将数据写入到 DiskLruCache 指定的文件中)。

DiskLruCache lruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
// 此处的 key 可以使用MD5对Url进行编码得到(只要保证唯一性即可)。
DiskLruCache.Editor editor = diskCache.edit(key)
try {// 对外提供的文件。File file = editor.getFile(0);// 将文件保存到 file 中。if (writer.write(file)) {// 保存成功后,DiskLruCache 也完成本次缓存写入操作。editor.commit();}
} finally {// 添加一条REMOVE操作记录editor.abortUnlessCommitted();
}

2.3 读取缓存

从 Value 中读取缓存。

DiskLruCache lruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
// 信息包装在 Value 对象里。
Value value = lruCache.get(key);
// 获取到key对应的缓存文件。
File result = value.getFile(0);// 一个key可以对应缓存多个文件,这个由构造DiskLruCache对象时传入的VALUE_COUNT决定。
public final class Value {private final String key; //keyprivate final long[] lengths; //对应每个缓存文件的大小,用于计算整个缓存的大小。private final File[] files; //对应每个缓存文件。
}

2.4 移除缓存

remove() 方法一般不主动触发,当缓存数据超过阈值时会自动触发。

DiskLruCache lruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
// 移除缓存。
lruCache .remove(key)

三、journal 日志格式

libcore.io.DiskLruCache
1
100
2CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

前五行是 journal 文件的Header:

  • 第一行:固定的字符串“libcore.io.DiskLruCache” (魔数,与字节码文件的魔数类似)。
  • 第二行:DiskLruCache 的版本号,值恒为1。
  • 第三行:应用程序的版本号,我们在 open() 方法里传入什么这里就会显示什么。
  • 第四行:每个key 可以对应几个文件,上面这里显示为2。
  • 第五行:是一个空行。

在分析第6行之前,我们先来了解下 CLEAN、DIRTY、REMOVE、READ 几个状态的含义。

  • CLEAN: 文件写入成功。调用 Editer.commit() 方法后,会向 journal 文件中写入一条 CLEAN 记录。
  • DIRTY: 表示当前缓存文件正在被写入。当我们调用DiskLruCache.edit() 方法时,都会向 journal 文件写入一条 DIRTY 记录。
  • REMOVE: 调用 abort() 方法表示缓存写入失败,会向 journal 文件写入一条 REMOVE 记录。
  • READ: 当我们调用 get() 方法去读取一条缓存时,就会向 journal 文件写入一条 READ 记录。

所以 DIRTY 335c4c6028171cfddfbaae1a9c313c52 的含义为:正在写入 key 为 335c4c6028171cfddfbaae1a9c313c52 的文件缓存。 CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 的含义为:key 为 3400330d1dfc7f3f7f4b8d4d803dfcf6 的文件缓存写入成功,且存在两个文件,文件大小分别为 832byte、21054byte。上面的 journal 文件支持一个 key 对应两个文件,所以 CLEAN 后面最多存在两个数字。

CLEAN 后面存放文件大小的意义: 当我们解析完 journal 日志文件后,就可以得到当前缓存目录下所有文件的大小。

到这里 journal 日志文件格式就介绍完了,下面我们来分析下具体的代码。


四、源码解析

在分析之前,我们先了解下关于 journal 文件操作涉及的三个文件:journaljournal.tmpjournal.bkp

  • 当 journal 文件重建时是通过journal.tmp 文件进行重建的。会将 journal 文件改名为 journal.bkp 文件,然后将 journal.tmp 改名为 journal 文件。
  • 当打开缓存时,如果 journal 文件不存在,则将 journal.bkp 文件改名为 journal 文件。
  • 在写入 CLEAN、REMOVE、READ、DIRTY 记录时,是写入 journal 文件。

下面主要分析几个方法:

  • DiskLruCache.open(File directory, int appVersion, int valueCount, long maxSize) :打开缓存。
  • DiskLruCache.edit(key) :写入缓存。
  • DiskLruCache.get(key) :获取缓存。
  • DiskLruCache.remove(key):移除缓存。
  • DiskLruCache.rebuildJournal() :journal 日志文件重建。

4.1 DiskLruCache.open()

DiskLruCache .open()

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)throws IOException {//...// 1.如果打开缓存时,有journal文件,则删除备份文件journal.bkp。// 如果打开缓存时,没有journal文件,则将备份文件journal.bkp 设置成journal文件。File backupFile = new File(directory, JOURNAL_FILE_BACKUP);if (backupFile.exists()) {File journalFile = new File(directory, JOURNAL_FILE);// If journal file also exists just delete backup file.if (journalFile.exists()) {backupFile.delete();} else {renameTo(backupFile, journalFile, false);}}// 2. journal文件解析// Prefer to pick up where we left off.DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);if (cache.journalFile.exists()) {try {// 解析journal文件,并删除REMOVE开始的数据。cache.readJournal();// 计算缓存的总大小&删除遗留的DIRTY文件(刚打开缓存时,DIRTY文件不允许存在)。cache.processJournal();return cache;} catch (IOException journalIsCorrupt) {cache.delete();}}// 3.兜底:如果上面还是没找到journal文件(第一次启动),则新建journal文件。// Create a new empty cache.directory.mkdirs();cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);cache.rebuildJournal();return cache;
}

DiskLruCache.readJournal()

private void readJournal() throws IOException {StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);try {// 读取前5行journal文件头,并作校验。String magic = reader.readLine();String version = reader.readLine();String appVersionString = reader.readLine();String valueCountString = reader.readLine();String blank = reader.readLine();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 + "]");}int lineCount = 0;while (true) {try {readJournalLine(reader.readLine());lineCount++; // 统计所有记录的行数。} catch (EOFException endOfJournal) {break;}}// 计算出当前的冗余数据行(总行数-有效的缓存个数)。redundantOpCount = lineCount - lruEntries.size();// If we ended on a truncated line, rebuild the journal before appending to it.// 判断journal文件是否有异常的行,如果存在就重新创建journal。if (reader.hasUnterminatedLine()) {rebuildJournal();} else {journalWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));}} finally {Util.closeQuietly(reader);}
}private void readJournalLine(String line) throws IOException {// 定位第一个空格int firstSpace = line.indexOf(' ');if (firstSpace == -1) {throw new IOException("unexpected journal line: " + line);}int keyBegin = firstSpace + 1;// 定位第2个空格,如果一行存在两个空格符,说明这一行是以CLEAN开头。int secondSpace = line.indexOf(' ', keyBegin);final String key;if (secondSpace == -1) {//说明这一行不是CLEAN开头。// 获取key。key = line.substring(keyBegin);// 移除journal文件中REMOVE的数据,说明文件缓存已经删除了,但是日志记录没有删除。// 这个在DiskLruCache.remove()方法中体现。if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {lruEntries.remove(key);return;}} else {key = line.substring(keyBegin, secondSpace);}// 将每一行的状态保存成Entry,并添加到LinkedHashMap中(LRU)。Entry entry = lruEntries.get(key);if (entry == null) {entry = new Entry(key);lruEntries.put(key, entry);}// 下面处理CLEAN、DIRTY、READ状态,REMOVE状态在上面就已经移除了,所以不做处理。// CLEAN开头,所以将readable属性设置为true,表示缓存已经写入成功可以让其他人访问。if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {// 截取文件大小String[] parts = line.substring(secondSpace + 1).split(" ");entry.readable = true;entry.currentEditor = null;// 这里是将文件大小设置到Entry中。entry.setLengths(parts);} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {// DIRTY开头,则为entry设置一个Editor(这个设置在后面processJournal()方法中会用到)。entry.currentEditor = new Editor(entry);} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {// READ开头的不处理。// This work was already done by calling lruEntries.get().} else {throw new IOException("unexpected journal line: " + line);}
}

小结:

·DiskLruCache.readJournal()· 方法做了下面几件事:

  1. 解析 journal 文件头并做校验。
  2. 解析 journal 文件,移除 REMOVE 开头的数据。并从 CLEAN、DIRTY、READ 开头的数据中获取 key、是否处于编辑状态、文件大小等信息并保存到 Entry。

DiskLruCache.processJournal()

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 {// 如果解析journal文件后仍存在DIRTY数据,则直接删除对应的缓存文件。// DIRTY文件不能单独存在,要么操作成功后在后面新增一条CLEAN操作;要么操作失败在后面新增一条REMOVE操作。entry.currentEditor = null;for (int t = 0; t < valueCount; t++) {deleteIfExists(entry.getCleanFile(t));deleteIfExists(entry.getDirtyFile(t));}i.remove();}}
}

小结:

  1. 遍历并统计缓存数据的总大小。
  2. 如果解析journal文件后仍存在 DIRTY 数据,则直接删除对应的缓存文件( DIRTY文件不能单独存在,要么操作成功后在后面新增一条CLEAN操作;要么操作失败在后面新增一条REMOVE操作。)。

4.2 DiskLruCache.edit(key)

写入缓存的伪代码: 含缓存添加成功和失败的场景。

DiskLruCache lruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
DiskLruCache.Editor editor = diskCache.edit(key)
try { // 1.对外提供缓存写入的File。File file = editor.getFile(0);if (writer.write(file)) {editor.commit(); // 2.缓存添加成功}
} finally { // 3.缓存添加失败editor.abortUnlessCommitted();
}

下面具体分析 DiskLruCache.edit(key) 方法:

DiskLruCache.edit(key)

// DiskLruCache.class
public Editor edit(String key) throws IOException {return edit(key, ANY_SEQUENCE_NUMBER);
}private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {checkNotClosed();//1.尝试从缓存中获取。Entry entry = lruEntries.get(key);if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null|| entry.sequenceNumber != expectedSequenceNumber)) {return null; // Value is stale.}// 2.没有缓存则创建一个Entry。if (entry == null) {entry = new Entry(key);lruEntries.put(key, entry);} else if (entry.currentEditor != null) {return null; // Another edit is in progress.}// 3.为entry.currentEditor赋值,说明当前这个key准备写入缓存了。Editor editor = new Editor(entry);entry.currentEditor = editor;// 4.向 journal文件中写入一条DIRTY操作记录。// Flush the journal before creating files to prevent file leaks.journalWriter.append(DIRTY);journalWriter.append(' ');journalWriter.append(key);journalWriter.append('\n');// 5.刷新一下,使新增记录写入文件。flushWriter(journalWriter);return editor;
}

一般在调用 edit() 方法后要调用 commit() 方法来完成事务。在分析 commit() 方法前我们先来看下 Editor.getFile(index) 方法。

Editor.getFile(index)

// Editor.class
public File getFile(int index) throws IOException {synchronized (DiskLruCache.this) {if (entry.currentEditor != this) {throw new IllegalStateException();}// 默认新增的缓存,entry.readable为false。if (!entry.readable) {// 重点:这里将written对应位置的文件设置为true,表明已经缓存成功了(实际上整个getFile()方法只是对外提供了一个File对象用于数据的缓存)。这个标记在 commit()方法中会用到。written[index] = true;}// 对外提供缓存的File是DirtyFile。File dirtyFile = entry.getDirtyFile(index);if (!directory.exists()) {directory.mkdirs();}return dirtyFile;}
}

Editor.commit()

// Editor.class
public void commit() throws IOException {// 传入true。completeEdit(this, true);// 下面这个标记用于在缓存失败后,abortUnlessCommitted()执行回滚时使用。committed = true;
}// Editor.class
public void abortUnlessCommitted() {if (!committed) {try {abort();} catch (IOException ignored) {}}
}// Editor.class
public void abort() throws IOException {// 传入false。completeEdit(this, false);
}// DiskLruCache.class
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++) {// 新增缓存时,如果没有调用Editor.getFile()方法,这里就会返回true,从而进入abort()方法。// 换句话说,你都没有对外提供一个文件供缓存写入,鬼知道缓存要放哪个文件。既然没有缓存文件,// 当然也不允许你在journal文件中插入操作日志了。if (!editor.written[i]) {editor.abort();throw new IllegalStateException("Newly created entry didn't create value for index " + i);}if (!entry.getDirtyFile(i).exists()) {editor.abort();return;}}}for (int i = 0; i < valueCount; i++) {File dirty = entry.getDirtyFile(i);// 缓存写入成功。if (success) {if (dirty.exists()) {// 缓存写入成功后,将DirtyFile更名为CleanFile。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 {// 缓存失败,则删除DirtyFile。deleteIfExists(dirty);}}redundantOpCount++;// 缓存写入完成后,将 entry.currentEditor置空,表示当前没有进行缓存操作。entry.currentEditor = null;if (entry.readable | success) {//写入CLEAN操作日志。entry.readable = true;journalWriter.append(CLEAN);journalWriter.append(' ');journalWriter.append(entry.key);journalWriter.append(entry.getLengths());journalWriter.append('\n');if (success) {entry.sequenceNumber = nextSequenceNumber++;}} else {//下面是缓存写入失败时触发,则在journal文件中写入REMOVE操作日志。lruEntries.remove(entry.key);journalWriter.append(REMOVE);journalWriter.append(' ');journalWriter.append(entry.key);journalWriter.append('\n');}// 将操作日志写入journal文件。flushWriter(journalWriter);// 触发缓存大小 或 日志重建校验逻辑。if (size > maxSize || journalRebuildRequired()) {executorService.submit(cleanupCallable);}
}// DiskLruCache.class
// 当冗余的信息操作2000条,且冗余信息比有效缓存数还多(即一半以上都是冗余的操作记录)。
private boolean journalRebuildRequired() {final int redundantOpCompactThreshold = 2000;return redundantOpCount >= redundantOpCompactThreshold //&& redundantOpCount >= lruEntries.size();
}

小结:

缓存写入一般分两种情况:成功 、 失败。

  1. 写入缓存前,需要通过 DiskLruCache.edit(key) 方法获取到 Editor 编辑对象,并向 journal 文件中写入一条 DIRTY 操作日志。
  2. 通过 Editor.getFile(index) 方法获取到缓存将要写入的文件 (这一步是将缓存文件与journal日志产生关联)。
  3. 通过IO流将缓存数据向第2步得到的文件写入,并判断缓存是否写入成功。
  4. 缓存写入成功,则调用 Editor.commit() 完成操作,并向 journal 文件中写入一条 CLEAN 操作日志。
  5. 缓存写入失败,则调用 Editor.abortUnlessCommitted() 完成操作,并向 journal 文件中写入一条 REMOVE 操作日志。

4.3 DiskLruCache.get(key)

// DiskLruCache.class
public synchronized Value get(String key) throws IOException {checkNotClosed();// 1.获取缓存Entry entry = lruEntries.get(key);if (entry == null) {return null;}// 当前文件不可读(只有CLEAN状态的文件才可以读取)。if (!entry.readable) {return null;}for (File file : entry.cleanFiles) {// A file must have been deleted manually!if (!file.exists()) {return null;}}// 向journal文件写入READ操作日志。redundantOpCount++;journalWriter.append(READ);journalWriter.append(' ');journalWriter.append(key);journalWriter.append('\n');if (journalRebuildRequired()) {executorService.submit(cleanupCallable);}// 将数据包装到Value对象中,实际的缓存文件就在entry.cleanFiles中。return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
}// Value.class
private Value(String key, long sequenceNumber, File[] files, long[] lengths) {this.key = key;this.sequenceNumber = sequenceNumber;this.files = files;this.lengths = lengths;
}
// Value.class
public File getFile(int index) {return files[index];
}

小结:获取缓存的流程

  1. 根据 key 获取缓存,并判断当前的缓存是否可读(entry.readable),只有缓存成功的文件才能被读取。
  2. 查找到缓存后,就向 journal 文件中写入一条 READ操作日志。
  3. 将查找到的文件缓存包装成一个 Value 并返回。

4.4 DiskLruCache.remove(key)

// DiskLruCache.class
public synchronized boolean remove(String key) throws IOException {checkNotClosed();// 1.获取缓存Entry entry = lruEntries.get(key);if (entry == null || entry.currentEditor != null) {return false;}// 删除key对应的缓存文件,并重新计算缓存size。for (int i = 0; i < valueCount; i++) {File file = entry.getCleanFile(i);// 这里删除了指定的缓存文件。if (file.exists() && !file.delete()) {throw new IOException("failed to delete " + file);}size -= entry.lengths[i];entry.lengths[i] = 0;}// 上面删除了缓存的文件,这里还要向journal文件写入REMOVE操作日志。// (所以前面提到了,在解析journal文件时要删除REMOVE开头的操作记录)redundantOpCount++;journalWriter.append(REMOVE);journalWriter.append(' ');journalWriter.append(key);journalWriter.append('\n');// 从LRU移除。lruEntries.remove(key);if (journalRebuildRequired()) {executorService.submit(cleanupCallable);}return true;
}

小结: 移除缓存的操作比较简单。

  1. 获取缓存,做一些条件校验。(如当前缓存正在写入时,不允许进行删除操作。
  2. 获取到缓存后,删除缓存文件,并向 journal 文件写入 REMOVE 操作日志。此时存在一种情况,即缓存文件不存在但是有 缓存文件对应的 REMOVE 操作记录,因此在解析 journal 文件时才会有删除 REMOVE 数据的操作。
  3. 从 lruEntries 链表中删除。

4.4 DiskLruCache.rebuildJournal()

private synchronized void rebuildJournal() throws IOException {// 1.重建日志前,需要关闭之前的写入流,因为操作的文件不一致,这里操作的是journal文件。if (journalWriter != null) {closeWriter(journalWriter);}// 2.新建写入流,操作journal.tmp文件,并写入journal文件头信息。Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));try {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");// 3.遍历 lruEntries链表,将操作日志写入journal.tmp文件。for (Entry entry : lruEntries.values()) {if (entry.currentEditor != null) {writer.write(DIRTY + ' ' + entry.key + '\n');} else {writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');}}} finally {// 4.关闭流。closeWriter(writer);}// 5.因为journal.tmp文件是我们想要的,所以要将之前的journal更改为journal.bkp。if (journalFile.exists()) {renameTo(journalFile, journalFileBackup, true);}// 然后将journal.tmp更名为journal。renameTo(journalFileTmp, journalFile, false);// 既然有了新的journal文件,journal.bkp 就可以删除了。journalFileBackup.delete();// 6.重新创建操作journal文件的写入流,用于记录读取、写入、移除缓存等操作。journalWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}

小结: journal 文件重建的流程:

  1. 关闭正在操作 journal 文件的写入流。
  2. 打开正在操作 journal.tmp 文件的写入流,并输入 journal 文件头信息(魔数、版本号等)。
  3. 遍历 lruEntries 链表,将内存中的数据缓存到 journal.tmp 文件。
  4. 关闭 journal.tmp 文件写入流。
  5. 将 journal.tmp 文件更名我 journal 文件,并删除之前的 journal 文件。
  6. 新建 journal 文件的写入流,用于记录读取、写入、移除缓存等操作。

到这里整个 DisLruCache 的操作就都已经分析完了。


五、总结

  1. 了解 DisLruCache 的几个常用 Api 。
  2. 了解 journal 文件的格式,熟悉缓存操作的几种日志格式。

DiskLruCache 源码分析相关推荐

  1. Android之DiskLruCache源码解析

    转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/73863258 本文出自:[顾林海的博客] 个人开发的微信小程序,目前功 ...

  2. 【OkHttp】OkHttp 源码分析 ( OkHttpClient.Builder 构造器源码分析 )

    OkHttp 系列文章目录 [OkHttp]OkHttp 简介 ( OkHttp 框架特性 | Http 版本简介 ) [OkHttp]Android 项目导入 OkHttp ( 配置依赖 | 配置 ...

  3. 图片加载框架Picasso - 源码分析

    简书:图片加载框架Picasso - 源码分析 前一篇文章讲了Picasso的详细用法,Picasso 是一个强大的图片加载缓存框架,一个非常优秀的开源库,学习一个优秀的开源库,,我们不仅仅是学习它的 ...

  4. okHttp3 源码分析

    一, 前言 在上一篇博客OkHttp3 使用详解里,我们已经介绍了 OkHttp 发送同步请求和异步请求的基本使用方法. OkHttp 提交网络请求需要经过这样四个步骤: 初始化 OkHttpClie ...

  5. Retrofit跟OkHttp源码分析

    网上已经有了相等多的分析博客,但终归是别人的知识点,倒不如自己走一遍流程,如果你看到了这篇博客,最好自己跟着思路对照源码过一遍哦! Retrofit源码分析 Retrofit的构建 在我们开发工作中使 ...

  6. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  7. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  8. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  9. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

  10. Yolov3Yolov4网络结构与源码分析

    Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...

最新文章

  1. maven scala plugin 实现jvmArgs,执行过程原理解析笔记
  2. 三层架构用户登录代码c语言,网站用户登录、注册和修改密码常用代码,采用三层架构...
  3. 初学 ASP.NET AJAX (一):构建 ASP.NET AJAX 开发环境
  4. JDBC之封装通用的BaseDao
  5. vue 实现 web端滚动刷新 排序 筛选 响应式布局 (源码)
  6. mysql数据库项目化教程郑小蓉_MySQL数据库项目化教程(高等职业教育“十三五”规划教材(软件技术专业))...
  7. junit单元测试断言_简而言之,JUnit:单元测试断言
  8. 专访Vue作者尤雨溪:Vue CLI 3.0重构的原因
  9. 广义表头尾链表存储结构_详解Redis五种数据结构的底层原理
  10. android+tv局域网播放器,【实用教程】电视盒子局域网播放全攻略
  11. JS的Date函数汇总
  12. 【37期】请你详细说说类加载流程,类加载机制及自定义类加载器
  13. Rainbow Fart安装及设置其他语音包
  14. 图像处理:双边滤波算法
  15. 外贸行业找客户的三种方式和五种工具
  16. Android音频子系统(十一)------耳机返听(耳返)原理实现
  17. 新东方mti百科知识pdf_新东方 2019考研英语 阅读理解精读100篇 基础版.pdf
  18. No module named ‘torchvision‘
  19. 转载(https://blog.csdn.net/qq_36738482/article/details/72823509)大数据的概念
  20. 2017计算机二级下半年,下半年全国计算机二级office题库及答案

热门文章

  1. 2021中国科技大学计算机博士招生,中国科学技术大学2021年拟录取博士研究生名单公示,2661人!...
  2. 使用火焰传感器和Arduino制作火灾探测器
  3. Linux:设置文件夹权限之777的含义
  4. 使用opencv和双目摄像头制作裸眼3d视频
  5. css样式的补充:鼠标悬停字体变大和改变颜色
  6. 教你使用Box2d制作用蜡笔手绘物体的效果(一)
  7. Timer 和TimerTask分析
  8. 电视盒子刷linux树莓派,变废为宝二:闲置“树莓派”开发板秒变电视盒子!
  9. 分数计算机在线应用,在线连分数计算器
  10. 通过图分析分散股票投资组合并降低风险增加收益