DiskLruCache 源码分析
文章目录
- 一、前言
- 二、基本使用
- 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 文件操作涉及的三个文件:journal
、journal.tmp
、journal.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()· 方法做了下面几件事:
- 解析 journal 文件头并做校验。
- 解析 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();}}
}
小结:
- 遍历并统计缓存数据的总大小。
- 如果解析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();
}
小结:
缓存写入一般分两种情况:成功 、 失败。
- 写入缓存前,需要通过
DiskLruCache.edit(key)
方法获取到 Editor 编辑对象,并向 journal 文件中写入一条 DIRTY 操作日志。 - 通过
Editor.getFile(index)
方法获取到缓存将要写入的文件 (这一步是将缓存文件与journal日志产生关联)。 - 通过IO流将缓存数据向第2步得到的文件写入,并判断缓存是否写入成功。
- 缓存写入成功,则调用
Editor.commit()
完成操作,并向 journal 文件中写入一条 CLEAN 操作日志。 - 缓存写入失败,则调用
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];
}
小结:获取缓存的流程
- 根据 key 获取缓存,并判断当前的缓存是否可读(entry.readable),只有缓存成功的文件才能被读取。
- 查找到缓存后,就向 journal 文件中写入一条 READ操作日志。
- 将查找到的文件缓存包装成一个 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;
}
小结: 移除缓存的操作比较简单。
- 获取缓存,做一些条件校验。(如当前缓存正在写入时,不允许进行删除操作。
- 获取到缓存后,删除缓存文件,并向 journal 文件写入 REMOVE 操作日志。此时存在一种情况,即缓存文件不存在但是有 缓存文件对应的 REMOVE 操作记录,因此在解析 journal 文件时才会有删除 REMOVE 数据的操作。
- 从 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 文件重建的流程:
- 关闭正在操作 journal 文件的写入流。
- 打开正在操作 journal.tmp 文件的写入流,并输入 journal 文件头信息(魔数、版本号等)。
- 遍历 lruEntries 链表,将内存中的数据缓存到 journal.tmp 文件。
- 关闭 journal.tmp 文件写入流。
- 将 journal.tmp 文件更名我 journal 文件,并删除之前的 journal 文件。
- 新建 journal 文件的写入流,用于记录读取、写入、移除缓存等操作。
到这里整个 DisLruCache 的操作就都已经分析完了。
五、总结
- 了解 DisLruCache 的几个常用 Api 。
- 了解 journal 文件的格式,熟悉缓存操作的几种日志格式。
DiskLruCache 源码分析相关推荐
- Android之DiskLruCache源码解析
转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/73863258 本文出自:[顾林海的博客] 个人开发的微信小程序,目前功 ...
- 【OkHttp】OkHttp 源码分析 ( OkHttpClient.Builder 构造器源码分析 )
OkHttp 系列文章目录 [OkHttp]OkHttp 简介 ( OkHttp 框架特性 | Http 版本简介 ) [OkHttp]Android 项目导入 OkHttp ( 配置依赖 | 配置 ...
- 图片加载框架Picasso - 源码分析
简书:图片加载框架Picasso - 源码分析 前一篇文章讲了Picasso的详细用法,Picasso 是一个强大的图片加载缓存框架,一个非常优秀的开源库,学习一个优秀的开源库,,我们不仅仅是学习它的 ...
- okHttp3 源码分析
一, 前言 在上一篇博客OkHttp3 使用详解里,我们已经介绍了 OkHttp 发送同步请求和异步请求的基本使用方法. OkHttp 提交网络请求需要经过这样四个步骤: 初始化 OkHttpClie ...
- Retrofit跟OkHttp源码分析
网上已经有了相等多的分析博客,但终归是别人的知识点,倒不如自己走一遍流程,如果你看到了这篇博客,最好自己跟着思路对照源码过一遍哦! Retrofit源码分析 Retrofit的构建 在我们开发工作中使 ...
- 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析
目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...
- SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)
[SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...
- SpringBoot-web开发(二): 页面和图标定制(源码分析)
[SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...
- SpringBoot-web开发(一): 静态资源的导入(源码分析)
目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...
- Yolov3Yolov4网络结构与源码分析
Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...
最新文章
- maven scala plugin 实现jvmArgs,执行过程原理解析笔记
- 三层架构用户登录代码c语言,网站用户登录、注册和修改密码常用代码,采用三层架构...
- 初学 ASP.NET AJAX (一):构建 ASP.NET AJAX 开发环境
- JDBC之封装通用的BaseDao
- vue 实现 web端滚动刷新 排序 筛选 响应式布局 (源码)
- mysql数据库项目化教程郑小蓉_MySQL数据库项目化教程(高等职业教育“十三五”规划教材(软件技术专业))...
- junit单元测试断言_简而言之,JUnit:单元测试断言
- 专访Vue作者尤雨溪:Vue CLI 3.0重构的原因
- 广义表头尾链表存储结构_详解Redis五种数据结构的底层原理
- android+tv局域网播放器,【实用教程】电视盒子局域网播放全攻略
- JS的Date函数汇总
- 【37期】请你详细说说类加载流程,类加载机制及自定义类加载器
- Rainbow Fart安装及设置其他语音包
- 图像处理:双边滤波算法
- 外贸行业找客户的三种方式和五种工具
- Android音频子系统(十一)------耳机返听(耳返)原理实现
- 新东方mti百科知识pdf_新东方 2019考研英语 阅读理解精读100篇 基础版.pdf
- No module named ‘torchvision‘
- 转载(https://blog.csdn.net/qq_36738482/article/details/72823509)大数据的概念
- 2017计算机二级下半年,下半年全国计算机二级office题库及答案
热门文章
- 2021中国科技大学计算机博士招生,中国科学技术大学2021年拟录取博士研究生名单公示,2661人!...
- 使用火焰传感器和Arduino制作火灾探测器
- Linux:设置文件夹权限之777的含义
- 使用opencv和双目摄像头制作裸眼3d视频
- css样式的补充:鼠标悬停字体变大和改变颜色
- 教你使用Box2d制作用蜡笔手绘物体的效果(一)
- Timer 和TimerTask分析
- 电视盒子刷linux树莓派,变废为宝二:闲置“树莓派”开发板秒变电视盒子!
- 分数计算机在线应用,在线连分数计算器
- 通过图分析分散股票投资组合并降低风险增加收益