图片框架 - Glide磁盘缓存研究
因为公司项目是基于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<R> 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<Key, String> 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<R> 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