因为公司项目是基于Glide4.8.0,所以这部分源码是基于4.8.0,而非之前文章的4.11.0,但是基本差不多。
这里以网络请求一张webp静图为例:
一、磁盘缓存执行流程
磁盘缓存原始数据调用栈(从上到下):
SourceGenerator.onDataReady
DecodeJob.reschedule runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
EngineJob.reschedule
DecodeJob.run
DecodeJob.runWrapped case SWITCH_TO_SOURCE_SERVICE: runGenerators()
SourceGenerator.startNext
SourceGenerator.cacheData
DiskCacheProvider.getDiskCache().put(key,new DataCacheWriter<>(encoder, toEncode, options));
DataCacheGenerator.startNext
ByteBufferFileLoader.loadData
DataCacheGenerator.onDataReady
DecodeJob.onDataFetcherReady
DecodeJob.decodeFromRetrievedData 如果不是同一个线程:case DECODE_DATA:decodeFromRetrievedData();
这里网络请求IO与磁盘IO并不是同一个线程,网络IO通过ActiveSourceExecutor,而磁盘IO通过diskCacheExecutor,因此这里DecodeJob重启一个线程去处理磁盘IO:EngineJob.reschedule。
网络请求之后,如果DiskCacheStrategy支持原始数据磁盘缓存,那么会走SourceGenerator.cacheData来进行缓存,然后从通过DataCacheGenerator从缓存中取原始数据通过DecodeJob.decodeFromRetrievedData进行解码。
磁盘缓存解码后数据流程:
private void decodeFromRetrievedData() {
Resource resource = null;
try {
resource = decodeFromData(currentFetcher, currentData, currentDataSource);
} catch (GlideException e) {
e.setLoggingDetails(currentAttemptingKey, currentDataSource);
throwables.add(e);
}
if (resource != null) {
notifyEncodeAndRelease(resource, currentDataSource);
} else {
runGenerators();
}
}
如果解码成功,resource不为空,走成功流程:
DecodeJob.notifyEncodeAndRelease
DeferredEncodeManager.encode
DiskCacheProvider.getDiskCache().put(key,new DataCacheWriter<>(encoder, toEncode, options));
这里最终会将转换后的图片进行编码,然后存储到磁盘。
如果resource为null,则证明解码失败,然后重新走DecodeJob的runGenerator方法,尝试重新找到对应的Generator来重新加载原始资源:这里会再重走SourceGenerator,但是此时由于hasNextModelLoader()为false的原因,没有更多的ModelLoader来加载数据,因此最终走finish流程,并返回失败回调。
调试截图:
总结一张时序图:
这里主要研究了原始数据和转换后数据磁盘缓存的触发时机。
同时得出结论:
- 网络请求成功才会磁盘缓存原始数据;
- 原始数据解码转换成功才会磁盘缓存转换后数据;
那么现在有一个问题,当网络请求成功后,原始数据会缓存磁盘,但是原始数据本身又有问题,导致解码失败,这样虽然不会缓存转换后数据,但是有问题的原始数据已经缓存了。下次加载会使用原始数据,而不会走网络请求。
问题解决方案思考:
- 给图片加signature,这种方式只是绕过去,但是并不会清理掉有问题的原始数据。
- 加载失败通过Glide.get(this).clearDiskCache();将磁盘缓存全部清理掉,这样会影响整体加载性能。
能不能在解码失败的时候,对当前图片已经缓存的原始数据进行单一清理,而不影响其他图片缓存数据?
那么接下来得研究下Glide磁盘缓存的做法。
二、磁盘缓存做法
磁盘写的地方在:
DiskCacheProvider.getDiskCache().put(key,new DataCacheWriter<>(encoder, toEncode, options));
DiskCacheProvider是一个接口,它的唯一实现类是LazyDiskCacheProvider
@Override
public DiskCache getDiskCache() {
if (diskCache == null) {
synchronized (this) {
if (diskCache == null) {
diskCache = factory.build();
}
if (diskCache == null) {
diskCache = new DiskCacheAdapter();
}
}
}
return diskCache;
}
这个factory对应InternalCacheDiskCacheFactory
@Override
public DiskCache build() {
File cacheDir = cacheDirectoryGetter.getCacheDirectory();
if (cacheDir == null) {
return null;
}
if (!cacheDir.mkdirs() && (!cacheDir.exists() || !cacheDir.isDirectory())) {
return null;
}
return DiskLruCacheWrapper.create(cacheDir, diskCacheSize);
}
DiskLruCacheWrapper.java
public static DiskCache create(File directory, long maxSize) {
return new DiskLruCacheWrapper(directory, maxSize);
}
最终操作在:DiskLruCacheWrapper.java,这里单独研究下put方法:
@Override
public void put(Key key, Writer writer) {
//1 资源唯一key的生成
String safeKey = safeKeyGenerator.getSafeKey(key);
Log.d("glidedisk","put: "+safeKey);
writeLocker.acquire(safeKey);
try {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Put: Obtained: " + safeKey + " for for Key: " + key);
}
try {
//2\. 初始化DiskLruCache
DiskLruCache diskCache = getDiskCache();
//如果已经存在,就不写入了
Value current = diskCache.get(safeKey);
if (current != null) {
return;
}
//3\. 文件写入逻辑
DiskLruCache.Editor editor = diskCache.edit(safeKey);
if (editor == null) {
throw new IllegalStateException("Had two simultaneous puts for: " + safeKey);
}
try {
File file = editor.getFile(0);
if (writer.write(file)) {
editor.commit();
}
} finally {
editor.abortUnlessCommitted();
}
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to put to disk cache", e);
}
}
} finally {
writeLocker.release(safeKey);
}
}
2.1资源唯一key的生成
SafeKeyGenerator.java
//这里用一个LruCache缓存生成好的key对应的safeKey
private final LruCache loadIdToSafeHash = new LruCache<>(1000);
public String getSafeKey(Key key) {
String safeKey;
synchronized (loadIdToSafeHash) {
safeKey = loadIdToSafeHash.get(key);
}
if (safeKey == null) {
//生成safeKey
safeKey = calculateHexStringDigest(key);//调用sha256BytesToHex()
}
synchronized (loadIdToSafeHash) {
loadIdToSafeHash.put(key, safeKey);
}
return safeKey;
}
这里主要通过SHA256算法来生成safeKey,属于散列算法,散列算法是一种单向密码,只有加密,没有解密。主要做文件一致性校验用。这里算法就不深入研究了。然后key和safeKey以key-value的形式缓存到LruCache(LinkedHashMap)。
2.2 DiskLruCache初始化
private synchronized DiskLruCache getDiskCache() throws IOException {
if (diskLruCache == null) {
diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
}
return diskLruCache;
}
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException {
if (maxSize <= 0L) {
throw new IllegalArgumentException("maxSize <= 0");
} else if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
} else {
//这里生成一个journal日志文件
File backupFile = new File(directory, "journal.bkp");
if (backupFile.exists()) {
File journalFile = new File(directory, "journal");
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();//读取日志文件,将记录数据写入LinkedHashMap
cache.processJournal();//处理日志文件
return cache;
} catch (IOException var8) {
System.out.println("DiskLruCache " + directory + " is corrupt: " + var8.getMessage() + ", removing");
cache.delete();//删除全部缓存文件
}
}
directory.mkdirs();//删除文件夹
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
}
DiskLruCache初始化时,先从日志文件中获取历史缓存以及读取顺序,之后再操作时也会同步更新到日志文件。
2.3 文件写入逻辑
DiskLruCache.Editor editor = diskCache.edit(safeKey);//存新数据到LinkedHashMap并往日志文件journal写入一笔状态为DIRTY的记录
if (editor == null) {
throw new IllegalStateException("Had two simultaneous puts for: " + safeKey);
}
try {
File file = editor.getFile(0);//获取dirtyFile
if (writer.write(file)) {将图片写入dirtyFile
editor.commit();//日志文件journal将DirtyFile重命名为CleanFile
}
cat看下journal文件:
这里dirty代表文件加入到了LinkedHashMap但是还没写入缓存文件,clean代表文件已经写入缓存文件
$:/data/data/com.stan.glidewebpdemo/cache/image_manager_disk_cache # cat journal
//dirty代表新写入LinkedHashMap的数据,后面是SHA256生成的key
DIRTY cb7bff7c7bb8f7020e0e7c7dfebc28b7fd4075d4882e756c5b28a655bb2525a3
//clean代表数据写入磁盘,key后的380850表示图片大小
CLEAN cb7bff7c7bb8f7020e0e7c7dfebc28b7fd4075d4882e756c5b28a655bb2525a3 380850
//read代表文件被读取
READ cb7bff7c7bb8f7020e0e7c7dfebc28b7fd4075d4882e756c5b28a655bb2525a3
//remove代表文件被删除
REMOVE 75489ed1dec1939efc93eec92aed0e9c76f9adc5e76b713f8687cde433b18fda
日志文件就像一个记事本,将每笔操作都记录下来,同时初始化的时候同步给到LinkedHashMap。
三、单独清理有问题的原始数据做法尝试
回到之前提出的问题,DiskLruCacheWrapper有个delete方法,但是没有对外暴露成api,该方法就是针对单个key进行删除,因为getSafeKey是SHA256算法生成,所以只要key相同,那么getSafeKey也是相同的。
@Override
public void delete(Key key) {
String safeKey = safeKeyGenerator.getSafeKey(key);
Log.d("glidedisk","delete: "+safeKey);
try {
getDiskCache().remove(safeKey);
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to delete from disk cache", e);
}
}
}
修改源码:
DecodeJob.java
private void decodeFromRetrievedData() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Retrieved data", startFetchTime,
"data: " + currentData
+ ", cache key: " + currentSourceKey
+ ", fetcher: " + currentFetcher);
}
Resource resource = null;
try {
resource = decodeFromData(currentFetcher, currentData, currentDataSource);
} catch (GlideException e) {
e.setLoggingDetails(currentAttemptingKey, currentDataSource);
throwables.add(e);
}
if (resource != null) {
notifyEncodeAndRelease(resource, currentDataSource);
} else {
diskCacheProvider.getDiskCache().delete(new DataCacheKey(currentSourceKey, signature));
runGenerators();
}
}
这里是对原始数据进行解码的入口方法。在resource == null时加入:
diskCacheProvider.getDiskCache().delete(new DataCacheKey(currentSourceKey, signature));
断点调试:
put之后:
打印:
2020-07-22 17:27:09.190 3006-3081/com.stan.glidewebpdemo D/glidedisk: put: 75489ed1dec1939efc93eec92aed0e9c76f9adc5e76b713f8687cde433b18fda
文件展示:
$:/data/data/com.stan.glidewebpdemo/cache/image_manager_disk_cache # ls -al
total 488
-rw------- 1 u0_a300 u0_a300_cache 81978 2020-07-22 17:27 75489ed1dec1939efc93eec92aed0e9c76f9adc5e76b713f8687cde433b18fda.0
-rw------- 1 u0_a300 u0_a300_cache 380850 2020-07-22 16:48 cb7bff7c7bb8f7020e0e7c7dfebc28b7fd4075d4882e756c5b28a655bb2525a3.0
-rw------- 1 u0_a300 u0_a300_cache 976 2020-07-22 17:27 journal
条件断点让resource == null,执行delete之后:
打印:
2020-07-22 17:28:06.734 3006-3081/com.stan.glidewebpdemo D/glidedisk: delete: 75489ed1dec1939efc93eec92aed0e9c76f9adc5e76b713f8687cde433b18fda
文件展示:
cepheus:/data/data/com.stan.glidewebpdemo/cache/image_manager_disk_cache # ls -al
total 400
-rw------- 1 u0_a300 u0_a300_cache 380850 2020-07-22 16:48 cb7bff7c7bb8f7020e0e7c7dfebc28b7fd4075d4882e756c5b28a655bb2525a3.0
-rw------- 1 u0_a300 u0_a300_cache 976 2020-07-22 17:27 journal