Glide源码学习笔记五:磁盘缓存策略

Glide源码学习笔记五:磁盘缓存

前言

上一章Glide源码学习笔记四:内存二级缓存跟进到内存的二级缓存逻辑,这一章来分析没有从内存获取到缓存的情况下进行的操作。本文中的源文件统一指未处理的原图文件
先帮大家回忆一下上一章的逻辑:

public <R> LoadStatus load(......) {
  	......
    synchronized (this) {
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
	  //没有获取到内存缓存,开始新的逻辑
      if (memoryResource == null) {
        return waitForExistingOrStartNewJob(......)
       }
    }

   ......
    return null;
  }

当没有获取到内存缓存的时候,调用了waitForExistingOrStartNewJob方法,从命名可以很好理解:等待正在进行的加载或者开始一个新的加载任务。
接下来进入waitForExistingOrStartNewJob的学习:

waitForExistingOrStartNewJob()

 private <R> LoadStatus waitForExistingOrStartNewJob(......) {
 	//Jobs中使用两个hashMap来保存EngineJob
    EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
    //如果之前已经开始过这个加载任务,就重新添加一次监听,等待这个任务的完成
    if (current != null) {
      current.addCallback(cb, callbackExecutor);
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Added to existing load", startTime, key);
      }
      return new LoadStatus(cb, current);
    }
	//创建EngineJob
    EngineJob<R> engineJob =
        engineJobFactory.build(......);
    //创建DecodeJob
    DecodeJob<R> decodeJob =
        decodeJobFactory.build(......);
	//将EngineJob保存起来,避免重复执行相同的加载任务
    jobs.put(key, engineJob);
	//添加监听
    engineJob.addCallback(cb, callbackExecutor);
    //开始任务
    engineJob.start(decodeJob);

    if (VERBOSE_IS_LOGGABLE) {
      logWithTimeAndKey("Started new load", startTime, key);
    }
    return new LoadStatus(cb, engineJob);
  }

上述代码中有几个关键类:
1.Jobs:保存已经执行的EngineJob对象,避免创建多个重复的EngineJob。内部是由两个HashMap构成。onlyCacheJobs和jobs,这两个HashMap是互斥关系,当设置onlyRetrieveFromCache=true使用onlyCacheJobs,反之使用jobs。这样处理有什么好处呢?
2.DecodeJob:从初始资源或者缓存中获取解码资源,将这些资源转换和解码,实现了Runnable。
3.EngineJob:当资源准备完毕,通过回调通知。通过添加回调或者移除回调管理加载。
于是waitForExistingOrStartNewJob这个方法做的事情就很好理解了:
一个加载请求对应一个EngineJob,EngineJob实现了很多回调,真正处理图片请求的是DecodeJob,加载任务的执行不同情况回调不同的方法,在这些回调方法中处理各种逻辑。
接下来分析DecodeJob的run方法,看看是如何处理资源的:

DecodeJob.run()

@Override
  public void run() {
    GlideTrace.beginSectionFormat("DecodeJob#run(reason=%s, model=%s)", runReason, model);
    DataFetcher<?> localFetcher = currentFetcher;
    try {
      if (isCancelled) {
        notifyFailed();
        return;
      }
      runWrapped();
    } catch (CallbackException e) {
      throw e;
    } catch (Throwable t) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(
            TAG,
            "DecodeJob threw unexpectedly" + ", isCancelled: " + isCancelled + ", stage: " + stage,
            t);
      }
      if (stage != Stage.ENCODE) {
        throwables.add(t);
        notifyFailed();
      }
      if (!isCancelled) {
        throw t;
      }
      throw t;
    } finally {
      if (localFetcher != null) {
        localFetcher.cleanup();
      }
      GlideTrace.endSection();
    }
  }

乍看之下,执行了runWrapped()方法,catch了一些异常。直接进入runWrapped()方法。

runWrapped()

分析runWrapped方法前,先看两个枚举:

 /** Why we're being executed again. */
  private enum RunReason {
    /** The first time we've been submitted. */
    INITIALIZE,
    /** We want to switch from the disk cache service to the source executor. */
    SWITCH_TO_SOURCE_SERVICE,
    /**
     * We retrieved some data on a thread we don't own and want to switch back to our thread to
     * process the data.
     */
    DECODE_DATA,
  }

RunReason :字面意思就是这个runnable为什么要再一次run,也就是说,成员变量RunReason根据状态改变,
在这里枚举了三种情况:
INITIALIZE:第一次提交一个Job,也就是刚刚engineJob.start(decodeJob)的情况。
SWITCH_TO_SOURCE_SERVICE:不从磁盘缓存获取资源了,直接获取源文件。
DECODE_DATA:在非当前线程获取到了资源,想切换到当前线程更新数据。

按照之前的线索,只能理解INITIALIZE是怎么回事,先直接翻译注释简单理解一下,知道大概怎么回事,带着困惑进行下面的分析,食用更香!

再看看Stage :

 /** Where we're trying to decode data from. */
  private enum Stage {
    /** The initial stage. */
    INITIALIZE,
    /** Decode from a cached resource. */
    RESOURCE_CACHE,
    /** Decode from cached source data. */
    DATA_CACHE,
    /** Decode from retrieved source. */
    SOURCE,
    /** Encoding transformed resources after a successful load. */
    ENCODE,
    /** No more viable stages. */
    FINISHED,
  }

Stage:这个是较好理解,解码的数据来源,和RunReason 一样也有一个Stage成员变量存储该枚举值,那这个阶段是什么阶段呢?
INITIALIZE:初始化阶段。
RESOURCE_CACHE:解码的数据来源是缓存中处理过的文件。这里就不是内存缓存了。因为内存缓存获取到了,就不会走到这里来,在上一章就已经结束了加载流程。那这个缓存肯定就是磁盘缓存了。
DATA_CACHE:解码数据来源是未处理过的源数据,这里先理解为从网络上download下来未处理的源数据缓存。
SOURCE:解码数据来源是源文件。
ENCODE:在成功下载资源并且缩放后,开始编码到本地。
FINISHED:解码这一阶段的流程结束。

了解这些枚举值代表的含义,接着看runWrapped()方法就会更加容易理解了。

 private void runWrapped() {
    switch (runReason) {
      //一开始触发DecodeJob的情况
      case INITIALIZE:
      //这里给stage赋值到下一个阶段
        stage = getNextStage(Stage.INITIALIZE);
        //currentGenerator是DataFetcherGenerator类型的,在DecodeJob的构造方法中默认没有赋值,这是第一次赋值
        currentGenerator = getNextGenerator();
        //这个方法下面再看
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        //这个方法下面再看
        decodeFromRetrievedData();
        break;
      default:
        //非正常情况,抛出异常
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }

从代码中可以很明显看出,runWrapped()就是根据runReason来确定stage,不同的stage对应不同的逻辑。
接下来依次分析getNextStage()getNextGenerator(),runGenerators(),decodeFromRetrievedData()

getNextStage()

private Stage getNextStage(Stage current) {
    switch (current) {
      case INITIALIZE:
        //如果缓存策略支持解码已缓存的资源,设置stage为RESOURCE_CACHE类型,若不支持,递归该方法,再次获取stage
        return diskCacheStrategy.decodeCachedResource()
            ? Stage.RESOURCE_CACHE
            : getNextStage(Stage.RESOURCE_CACHE);
      case RESOURCE_CACHE:
       //如果缓存策略支持解码已缓存的源文件,设置stage为DATA_CACHE类型,若不支持,递归该方法,再次获取stage
        return diskCacheStrategy.decodeCachedData()
            ? Stage.DATA_CACHE
            : getNextStage(Stage.DATA_CACHE);
      case DATA_CACHE:
      //如果设置了只能从缓存中读取,设置stage为FINISHED否则设置为SOURCE去获取源文件
        // Skip loading from source if the user opted to only retrieve the resource from cache.
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
  }

diskCacheStrategy这个东西应该很熟悉了,缓存策略。diskCacheStrategy的一些属性很好理解,在这里就不解释了,可以自己打开源码看一看。总而言之,getNextStage方法就是根据不同缓存策略设置不同的stage。
其实看到了这里,磁盘的缓存策略已经很清晰了:
1.先去磁盘缓存中寻找已经处理好了资源,没有找到执行2
2.寻找缓存中的源文件,没有找到执行3
3.直接获取源文件,没有获取到,肯定就去执行下载操作了
接下来分析getNextGenerator()。

getNextGenerator()

  private DataFetcherGenerator getNextGenerator() {
    switch (stage) {
      case RESOURCE_CACHE:
        return new ResourceCacheGenerator(decodeHelper, this);
      case DATA_CACHE:
        return new DataCacheGenerator(decodeHelper, this);
      case SOURCE:
        return new SourceGenerator(decodeHelper, this);
      case FINISHED:
        return null;
      default:
        throw new IllegalStateException("Unrecognized stage: " + stage);
    }
  }

逻辑很清晰,已经获取到当前情况的stage,接下来就是根据stage执行不同的磁盘资源获取方式。

runGenerators()

  private void runGenerators() {
    currentThread = Thread.currentThread();
    startFetchTime = LogTime.getLogTime();
    boolean isStarted = false;
    while (!isCancelled
        && currentGenerator != null
        && !(isStarted = currentGenerator.startNext())) {
      stage = getNextStage(stage);
      currentGenerator = getNextGenerator();

      if (stage == Stage.SOURCE) {
        reschedule();
        return;
      }
    }
    // We've run out of stages and generators, give up.
    if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
      notifyFailed();
    }

    // Otherwise a generator started a new load and we expect to be called back in
    // onDataFetcherReady.
  }

执行runGenerators方法,内部有一个while循环,跳出循环由两种情况,第一种,currentGenerator.startNext()为true,该方法设计到了缓存获取以及下载流程,放在下一章考虑。第二种就是stage == Stage.SOURCE,执行了reschedule()方法,来看看:
该方法

reschedule()


  @Override
  public void reschedule() {
    runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
    //回调,在EngineJob的reschedule处理
    callback.reschedule(this);
  }
  
  @Override
  public void reschedule(DecodeJob<?> job) {
    // Even if the job is cancelled here, it still needs to be scheduled so that it can clean itself
    // up.
    //重新开始这个任务
    getActiveSourceExecutor().execute(job);
  }

这其实就是处理异常了,磁盘缓存未获取到资源,后续操作也没有获取到资源,于是重新开始该任务。

decodeFromRetrievedData()

当runReason改变为DECODE_DATA,也就是可以获取到资源,需要回调处理资源了。

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, isLoadingFromAlternateCacheKey);
    } else {
      runGenerators();
    }
  }

这个回调链太长了,最终会回调到Engine的onEngineJobComplete()方法,看看有什么惊喜:

Engine.onEngineJobComplete()

@SuppressWarnings("unchecked")
  @Override
  public synchronized void onEngineJobComplete(
      EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    // A null resource indicates that the load failed, usually due to an exception.
    if (resource != null && resource.isMemoryCacheable()) {
      activeResources.activate(key, resource);
    }
    jobs.removeIfCurrent(key, engineJob);
  }

看到这个activeResources是不是异常熟悉?这个就是上一章内存缓存的第一级缓存。这样,内存缓存与磁盘缓存就打通了。

总结

在这里插入图片描述

本文主要从源码角度分析了磁盘缓存的策略。若大家有不同意见,欢迎讨论,一起学习一起进步!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值