美文网首页
图片框架 - Glide磁盘缓存研究

图片框架 - Glide磁盘缓存研究

作者: Stan_Z | 来源:发表于2020-07-22 18:47 被阅读0次

    因为公司项目是基于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
    

    相关文章

      网友评论

          本文标题:图片框架 - Glide磁盘缓存研究

          本文链接:https://www.haomeiwen.com/subject/zdvtlktx.html