图片框架 - 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 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

你可能感兴趣的:(图片框架 - Glide磁盘缓存研究)