Glide圖片磁盤緩存
在《Glide實(shí)現(xiàn)WebView離線圖片的酷炫展示效果》一文中昧谊,在webview中的圖片我們通過(guò)Glide緩存起來(lái),并且將html的內(nèi)容保存到文件,最終可以在離線下實(shí)現(xiàn)文章閱讀血公。其中的圖片資源通過(guò)glide緩存到cache目錄下隐解,我們知道邪媳,Glide在加載網(wǎng)絡(luò)圖片時(shí)可以將圖片緩存到內(nèi)存與磁盤中。當(dāng)我們下次使用時(shí)叽唱,就可以從緩存中獲取顯示圖片。其中的磁盤緩存使用的是DiskLruCache算法微宝。
?但是緩存文件總大小是有限制的棺亭,當(dāng)超過(guò)250M(默認(rèn))時(shí),會(huì)根據(jù)LRU算法刪除一些久未使用的圖片資源蟋软。但是這樣會(huì)導(dǎo)致離線情況下無(wú)法加載一些WebView中的圖片(如果)镶摘。因此我們希望的是將如果html頁(yè)面被離線保存下來(lái),那么其中的圖片也把它放到一個(gè)永久保存的目錄下岳守,這些圖片資源不會(huì)記錄到LRU中凄敢。
Glide磁盤緩存過(guò)程源碼分析
?為了能夠?qū)崿F(xiàn)上面的圖片永久存儲(chǔ)效果,就需要自定義磁盤緩存策略湿痢。在這之前涝缝,我們需要大致了解下Glide的源碼,它是如何將圖片緩存到磁盤譬重,又如何使用磁盤中的圖片資源的拒逮。
Glide.with(this).load(url).into(image)
通過(guò)AS調(diào)試into
方法,發(fā)現(xiàn)最終會(huì)調(diào)用RequestManager
的track
方法,request
實(shí)際類型為SingleRequest
:
進(jìn)入track
方法臀规,發(fā)現(xiàn)執(zhí)行的是SingleRequest
的begin
方法
跟蹤調(diào)試SingleRequest
的begin
方法, 執(zhí)行了DrawableImageViewTarget
的getSize
方法滩援,并且把自己作為回調(diào)參數(shù)傳入,類型為SizeReadyCallback
,猜測(cè)估計(jì)最終調(diào)用onSizeReady
方法塔嬉。
通過(guò)AS快捷鍵搜索(mac使用command+o)DrawableImageViewTarget
類玩徊,然后搜索getSize
方法(command+F12,輸入getSize)租悄,找到對(duì)應(yīng)的實(shí)現(xiàn)類ViewTarget
。
繼續(xù)調(diào)試ViewTarget
方法佣赖,可以發(fā)現(xiàn)是通過(guò)ViewTreeObserver
獲取ImageView
獲取大小恰矩,然后最終回調(diào)SingleRequest
的onSizeReady
方法。
回到SingleRequest
的onSizeReady
方法憎蛤。status
置為RUNNING
狀態(tài)外傅。然后執(zhí)行Engine
中的load方法
,繼續(xù)跟進(jìn)俩檬,發(fā)現(xiàn)執(zhí)行了DecodeJob
的runWrapped
方法萎胰。
在runGenerators
方法中,循環(huán)執(zhí)行了currentGenerator.startNext()
方法棚辽。
DataFetcherGenerator
接口的實(shí)現(xiàn)類有三個(gè)
- ResourceCacheGenerator
- DataCacheGenerator
- SourceGenerator
而在SourceGenerator
的startNext
會(huì)執(zhí)行cacheToData
緩存磁盤操作,通過(guò)DiskLruCacheWrapper
的put
方法將圖片資源緩存到磁盤技竟。
glide-DiskLruCacheWrapper-put.png
最終的磁盤緩存算法是通過(guò)DiskLruCache
類來(lái)實(shí)現(xiàn)。
DiskLruCache
Glide通過(guò)DiskLruCache
將圖片的原始資源屈藐,以及一些指定執(zhí)行尺寸的資源緩存至磁盤榔组。主要包含一個(gè)名為journal
的索引文件,以及多個(gè)經(jīng)過(guò)hash命名后的圖片資源文件联逻。
一個(gè)journal
文件可能是這樣的:
libcore.io.DiskLruCache
1
100
2
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
前4行分別表示文件頭信息搓扯,disk cache
版本,應(yīng)用版本()包归,每個(gè)緩存項(xiàng)的數(shù)量(valueCount
)锨推。
第6行開始表示了不同狀態(tài)的緩存記錄,每條記錄相關(guān)信息通過(guò)空格隔開:
狀態(tài)+key+指定狀態(tài)的相關(guān)屬性
-
DIRTY
表示數(shù)據(jù)正在創(chuàng)建或者更新操作公壤,每個(gè)DIRTY
臟操作之后都應(yīng)該有一個(gè)CLEAN
或者REMOVE
操作换可,沒有匹配的CLEAN
或REMOVE
的臟行表示可能需要?jiǎng)h除臨時(shí)文件。 -
CLEAN
表示已成功發(fā)布并可能被讀取的緩存項(xiàng)厦幅,后面表示的是每個(gè)值的長(zhǎng)度沾鳄,如果valueCount
為2,有兩個(gè)文件key.0
和key.1
确憨。后面兩個(gè)數(shù)值表示文件長(zhǎng)度洞渔。 -
READ
表示圖片的讀取記錄,它會(huì)進(jìn)入LRU缚态。 -
REMOVE
表示緩存資源的刪除操作磁椒。
默認(rèn)的DiskLruCache
緩存的最大值為250M
,當(dāng)圖片文件總長(zhǎng)度操作這一數(shù)值是玫芦,就會(huì)進(jìn)行REMOVE
操作浆熔。
//DiskLruCache.java
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
如果我們離線緩存的圖片需要永久使用,那就不能把那些離線圖片計(jì)入LRU中,不然医增,當(dāng)有一天慎皱,緩存文件超出250M,一些離線圖片就會(huì)被刪除叶骨。因此我們需要定義一個(gè)新的PERMANENT
(永久)狀態(tài)茫多,不會(huì)進(jìn)入LRU,并且忽刽,我們將這些圖片放入另外的文件夾下面天揖。
自定義DiskLruCache支持永久存儲(chǔ)
首先我們需要使得Glide支持自定義DiskCache
,使用@GlideModule
定義一個(gè)AppGlideModule
跪帝,然后在applyOptions
方法中實(shí)現(xiàn)緩存自定義今膊。
@GlideModule
public class MyAppGlideModule extends AppGlideModule {
@Override
public void applyOptions(@NonNull Context context,
@NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDiskCache(new WanDiskCacheFactory(new WanDiskCacheFactory.CacheDirectoryGetter() {
@NotNull
@Override
public File getCacheDirectory() {
return new File(context.getCacheDir(), "wandroid_images");
}
@NotNull
@Override
public File getPermanentDirectory() {
return new File(context.getFilesDir(), "permanent_images");
}
}));
}
}
修改DiskCacheFactory
源碼,改為WanDiskCacheFactory
,新增permanentDirectory
目錄
class WanDiskCacheFactory(var cacheDirectoryGetter: CacheDirectoryGetter) :
DiskCache.Factory {
interface CacheDirectoryGetter {
val cacheDirectory: File
val permanentDirectory: File
}
override fun build(): DiskCache? {
val cacheDir: File =
cacheDirectoryGetter.cacheDirectory
val permanentDirectory = cacheDirectoryGetter.permanentDirectory
cacheDir.mkdirs()
permanentDirectory.mkdirs()
return if ((!cacheDir.exists()
|| !cacheDir.isDirectory
|| !permanentDirectory.exists()
|| !permanentDirectory.isDirectory)
) {
null
} else WanDiskLruCacheWrapper.create(
permanentDirectory,
cacheDir,
20 * 1024 * 1024//262144000L(250M) for cache
)
}
}
修改DiskLruCache
為WanDiskLruCache
伞剑,添加PERMANENT
字段斑唬,使得它支持圖片永久存儲(chǔ)操作。
public final class WanDiskLruCache implements Closeable {
static final String MAGIC = "libcore.io.WanDiskLruCache";
...
private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";
private static final String PERMANENT = "PERMANENT";//長(zhǎng)久文件(不進(jìn)入LRU)
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;
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
permanentEntries.remove(key);
return;
}
//永久區(qū)
if (firstSpace == PERMANENT.length() && line.startsWith(PERMANENT)) {
lruEntries.remove(key);
Entry entry = permanentEntries.get(key);
if (entry == null) {
entry = new Entry(key, permanentDirectory);
entry.readable = true;
permanentEntries.put(key, entry);
}
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
...
}
public synchronized Value get(String key) throws IOException {
checkNotClosed();
Entry permanentEntry = readPermanentEntry(key);
if (permanentEntry != null) {
StringKt.logV("read from permanent permanent directory:" + key);
return new Value(key, permanentEntry.sequenceNumber,
permanentEntry.cleanFiles,
permanentEntry.lengths);
}
...
}
/**
* 讀取永久區(qū)的文件
*
* @param key
* @return
*/
private Entry readPermanentEntry(String key) throws IOException {
Entry entry = permanentEntries.get(key);
if (entry == null) {
entry = new Entry(key, permanentDirectory);
entry.readable = true;
for (File file : entry.cleanFiles) {
// A file must have been deleted manually!
if (!file.exists()) {
return null;
}
}
addOpt(PERMANENT, key);
}
return entry;
}
/**
* 將緩存的文件移動(dòng)到permanent下
*/
public synchronized boolean cacheToPermanent(String key) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
StringKt.logV("cacheToPermanent null:" + key);
return false;
}
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists()) {
FileUtil.copyFileToDirectory(file, permanentDirectory);
file.delete();
}
size -= entry.lengths[i];
StringKt.logV("cacheToPermanent:" + entry.getLengths() + ",key:" + entry.key);
entry.lengths[i] = 0;
}
Entry pEntry = new Entry(key, permanentDirectory);
pEntry.readable = true;
permanentEntries.put(key, pEntry);
lruEntries.remove(key);
addOpt(PERMANENT, key);
return true;
}
/**
* 刪除永久圖片
*
* @param key
* @return
* @throws IOException
*/
public synchronized boolean removePermanent(String key) throws IOException {
checkNotClosed();
Entry entry = readPermanentEntry(key);
if (entry == null) return false;
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists() && !file.delete()) {
throw new IOException("failed to delete " + file);
}
permanentEntries.remove(key);
}
addOpt(REMOVE, key);
return true;
}
/**
* 將下載好的tmp文件放入永久區(qū)
*/
public synchronized boolean tempToPermanent(File tmp, String key) throws IOException {
StringKt.logV("tempToPermanent:" + key);
Entry entry = new Entry(key, permanentDirectory);
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
tmp.renameTo(file);
}
permanentEntries.put(key, entry);
addOpt(PERMANENT, key);
return true;
}
}
在通過(guò)Glide實(shí)現(xiàn)WebView離線圖片展示時(shí)黎泣,將圖片資源永久存儲(chǔ)有兩種情況恕刘,一種是圖片已經(jīng)加載,磁盤中已經(jīng)有相關(guān)資源抒倚,我們可以通過(guò)cacheToPermanent
方法褐着,將圖片從緩存目錄移動(dòng)到永久目錄。另外一種是圖片未加載衡便,這種情況離線緩存的話需要下載圖片献起,然后tempToPermanent
直接放入永久區(qū)域洋访。