An overview

The above article continues with the analysis of disk caching. First, in the last article we knew where the cache class was created, so I’ll use that as a clue to where it ends up being called. Because the disk cache after multiple parameter transfer, not very good to find, so first through its construction to find, this is also read the source code of an idea.

Disk cache class analysis

GliderBuilder create

  • DiskCacheFactory inheritance DiskLruCacheFactory
  • The cache directory creation method is internally rewritten
  • DiskLruCache class related to the principle of caching, more online articles, interested can search view
if (diskCacheFactory == null) {
      diskCacheFactory = new InternalCacheDiskCacheFactory(context);
    }

Copy the code

Passed on to the Engine

engine = new Engine(memoryCache,diskCacheFactory,...) ;Copy the code

Package through LazyDiskCacheProvider

  • The factory class that creates DiskLruCache later becomes diskCacheProvider
//Engine constructor this.diskCacheProvider = new LazyDiskCacheProvider(diskCacheFactory);Copy the code

Passed to the DecodeJobFactory

  • DecodeJob is the core class responsible for data loading processing
  • DecodeJobFactory is the static inner class of Engine
// The constructor of Engineif (decodeJobFactory == null) {
      decodeJobFactory = new DecodeJobFactory(diskCacheProvider);
    }
Copy the code

When creating a DecodeJob, set diskCacheProvider

  • DecodeJob is created in engine.load according to the flowchart
  • DecodeJob has decodeJobFactory.build created
DecodeJob<R> decodeJob =decodeJobFactory.build();
Copy the code
  • Get the DecodeJob instance from Pools
  • Here we can learn a way to build objects
build(...) { DecodeJob<R> result = Preconditions.checkNotNull((DecodeJob<R>) pool.acquire()); } final Pools.Pool<DecodeJob<? >> pool = FactoryPools.threadSafe( JOB_POOL_SIZE, new FactoryPools.Factory<DecodeJob<? >>() { @Override public DecodeJob<? >create() {
                returnnew DecodeJob<>(diskCacheProvider, pool); }});Copy the code

Call method of the cache add method

– the above package type LazyDiskCacheProvider for DecodeJob. DiskCacheProvider interface implementation class

  • We passed it on to DecodeJob
  • The interface method overridden by LazyDiskCacheProvider gets the cache class DiskCache from the diskCacheFactory passed by GlideBuild
private static class LazyDiskCacheProvider implements DecodeJob.DiskCacheProvider

interface DiskCacheProvider {
    DiskCache getDiskCache();
  }
  
  public DiskCache getDiskCache() {
      if (diskCache == null) {
        synchronized (this) {
          if (diskCache == null) {
            diskCache = factory.build();
          }
          if(diskCache == null) { diskCache = new DiskCacheAdapter(); }}}returndiskCache; }}Copy the code
  • Add a call to the cache method put
void encode(DiskCacheProvider diskCacheProvider, Options options) {
      GlideTrace.beginSection("DecodeJob.encode"); try { diskCacheProvider .getDiskCache() .put(key, new DataCacheWriter<>(encoder, toEncode, options)); } finally { toEncode.unlock(); GlideTrace.endSection(); }}Copy the code

Here we know that the disk cache is added in DecodeJob encode method, we remember this method, that it is called when, we go back to the data loading process, start analysis, but also can find the disk cache read.

Read from disk cache

Through the process analysis of the last article, we know that data loading is finally processed by three core methods in DecodeJob :getNextStage; getNextGenerator(); runGenerators

  • GetNextStage will proceed to the next level according to the current state and process it step by step. This is the control method of disk cache policy configuration. The default state is initialization
case INITIALIZE:
        return diskCacheStrategy.decodeCachedResource()
            ? Stage.RESOURCE_CACHE
            : getNextStage(Stage.RESOURCE_CACHE);
case RESOURCE_CACHE:
        return diskCacheStrategy.decodeCachedData()
            ? Stage.DATA_CACHE
            : getNextStage(Stage.DATA_CACHE);
case DATA_CACHE:
        // Skip loading from source if the user opted to only retrieve the resource from cache.
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
Copy the code
  • GetNextGenerator will create classes for different states
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); }}Copy the code
  • RunGenerators, internal will cycle, call getNextState, getNextGenerator, until the corresponding processing class return the correct data, when is the last Stage. The Source, return to EngineJob, then call again DecodeJob, state straight at this time Connect into SWITCH_TO_SOURCE_SERVICE, said there was no access to the cache, call the SourceGenerator. StatrNext method to get the data.
  • Write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write write
  • StartNext, first generate the corresponding key, then call loadData.fetch. LoadData
 Key originalKey = new DataCacheKey(sourceId, helper.getSignature()); cacheFile = helper.getDiskCache().get(originalKey) cacheFile = helper.getDiskCache().get(originalKey); ModelLoader<File, ? > modelLoader = modelLoaders.get(modelLoaderIndex++); loadData = modelLoader.buildLoadData( cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());if(loadData ! = null && helper.hasLoadPath(loadData.fetcher.getDataClass())) { started =true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }

Copy the code
  • Glide in the last article we analyzed the construction method, explained that it registered in the various types of loader, its acquisition process is more cumbersome, we directly corresponding to the class view, disk cache, obviously is FileLoader
  • FileLoader’s buildData method returns FileFetcher, which is a static inner class
@Override
  public LoadData<Data> buildLoadData(
      @NonNull File model, int width, int height, @NonNull Options options) {
    return new LoadData<>(new ObjectKey(model), new FileFetcher<>(model, fileOpener));
  }
Copy the code
  • A loadData method of the FileFecher class that retrieves data based on the resulting cacheFile
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super Data> callback) {
      try {
        data = opener.open(file);
      } catch (FileNotFoundException e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "Failed to open file", e);
        }
        callback.onLoadFailed(e);
        return;
      }
      callback.onDataReady(data);
    }
Copy the code
  • The next step is the callback sequence, which is roughly the same as the network request callback logic, as you can see from the flow diagram, finally set to imageView, analysis write will expand later

At this point, the disk cache has been read and there are two types of disk caches. One is Resource, one is Data. The specific difference depends on the class annotation. Code comments and naming are great for readability

/**
 * Generates {@link com.bumptech.glide.load.data.DataFetcher DataFetchers} from cache files
 * containing downsampled/transformed resource data.
 */
class ResourceCacheGenerator implements DataFetcherGenerator, DataFetcher.DataCallback<Object> 

/**
 * Generates {@link com.bumptech.glide.load.data.DataFetcher DataFetchers} from cache files
 * containing original unmodified source data.
 */
 class DataCacheGenerator implements DataFetcherGenerator, DataFetcher.DataCallback<Object> 

Copy the code

Write to disk

If the disk cache is not called the first time, the startNext method of SourceGenerator will be called from the above analysis. Look for loadData inside and call loadData.fetch. LoadData

  • What loadData is,fetch is the class that we mentioned in the Glide constructor in the last article, so let’s look at it step by step.
while(! started && hasNextModelLoader()) { loadData = helper.getLoadData().get(loadDataListIndex++);if(loadData ! = null && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource()) || helper.hasLoadPath(loadData.fetcher.getDataClass()))) { started =true; loadData.fetcher.loadData(helper.getPriority(), this); }}Copy the code

Add and find Loader

Loader to add
  • Glide’s construction method is added via regeister. Append
  • It passes three arguments, the first Model. Class, the second data type class, and the third factory class
registry.append(ByteBuffer.class, new ByteBufferEncoder())
....
 .append(String.class, InputStream.class, new StringLoader.StreamFactory())
.append(Uri.class, InputStream.class, new UrlUriLoader.StreamFactory())
        .append(URL.class, InputStream.class, new UrlLoader.StreamFactory())
        .append(Uri.class, File.class, new MediaStoreFileLoader.Factory(context))
        .append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())
Copy the code
  • Regeister. Add method calls ModelLoaderRegister append. Append method
 @NonNull
  public <Model, Data> Registry append(
      @NonNull Class<Model> modelClass,
      @NonNull Class<Data> dataClass,
      @NonNull ModelLoaderFactory<Model, Data> factory) {
    modelLoaderRegistry.append(modelClass, dataClass, factory);
    return this;
  }
Copy the code
  • In ModelLoaderRegister. Append method call MultiModelLoaderFactory append method
synchronized <Model, Data> void append(
      @NonNull Class<Model> modelClass,
      @NonNull Class<Data> dataClass,
      @NonNull ModelLoaderFactory<? extends Model, ? extends Data> factory) {
    add(modelClass, dataClass, factory, /*append=*/ true);
  }
Copy the code
  • The add method of MultiModelLoaderFactory adds it to the Entry
  • ==Entry has three variables, which is the parameter == that we passed in Glide
  • I add here and I’m done
private <Model, Data> void add(
      @NonNull Class<Model> modelClass,
      @NonNull Class<Data> dataClass,
      @NonNull ModelLoaderFactory<? extends Model, ? extends Data> factory,
      boolean append) {
    Entry<Model, Data> entry = new Entry<>(modelClass, dataClass, factory);
    entries.add(append ? entries.size() : 0, entry);
  }
Copy the code
The loader to find

We get the network picture, we pass it as a string, and the data type is stream

  • The sourcegenerator startNext lookup, takes traversal method, found not qualified, will increase, to obtain, we remember the judgment condition here, behind open analysis
  • Helper.getloaddata () calls the register.getModelLoader method, gets the modelLoaders, and iterates through the buildLoadData method of each loader
 while(! started && hasNextModelLoader()) { loadData = helper.getLoadData().get(loadDataListIndex++);if(loadData ! = null && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource()) || helper.hasLoadPath(loadData.fetcher.getDataClass()))) { started =true; loadData.fetcher.loadData(helper.getPriority(), this); } } List<LoadData<? >>getLoadData() {
    if(! isLoadDataSet) { isLoadDataSet =true; loadData.clear(); List<ModelLoader<Object, ? >> modelLoaders = glideContext.getRegistry().getModelLoaders(model); //noinspection ForLoopReplaceableByForEach to improve perffor(int i = 0, size = modelLoaders.size(); i < size; i++) { ModelLoader<Object, ? > modelLoader = modelLoaders.get(i); LoadData<? > current = modelLoader.buildLoadData(model, width, height, options);if(current ! = null) { loadData.add(current); }}}return loadData;
  }
 
Copy the code
  • Register getModelLoaders will call ModelLoaderRegister getModelLoaders method, USES the entrusted design patterns, the register is added and obtain through ModelLoaderRegister
  • ModelLoaderRegister getModelLoadersForClass getModelLoaders internal call
@NonNull public <Model> List<ModelLoader<Model, ? >> getModelLoaders(@NonNull Model model) { List<ModelLoader<Model, ? >> result = modelLoaderRegistry.getModelLoaders(model);if (result.isEmpty()) {
      throw new NoModelLoaderAvailableException(model);
    }
    return result;
  }
  
   public <A> List<ModelLoader<A, ?>> getModelLoaders(@NonNull A model) {
    List<ModelLoader<A, ?>> modelLoaders = getModelLoadersForClass(getClass(model));
    ····
    
  }
Copy the code
  • GetModelLoadersForClass internal calls MultiModelLoaderFactory. The build method, which will be introduced into the model type, and through the DecodeHeelper incoming, and at the time of the DecodeJob tectonic DecodeHelper, gen Build, pass in. So we know it’s string.class
  • In MultiModelLoaderFactory. The build method will be called entry. The build method
  • Class is string. Class has multiple StringLoader classes. Now we’re going to go to the StringLoader class
synchronized <Model> List<ModelLoader<Model, ? >> build(@NonNull Class<Model> modelClass) { try { List<ModelLoader<Model, ? >> loaders = new ArrayList<>();for(Entry<? ,? > entry : entries) {if(entry.handles(modelClass)) { alreadyUsedEntries.add(entry); Loaders. add(this.<Model, Object>build(entry)); alreadyUsedEntries.remove(entry); }}returnloaders; } private <Model, Data> ModelLoader<Model, Data> build(@NonNull Entry<? ,? > entry) {return (ModelLoader<Model, Data>) Preconditions.checkNotNull(entry.factory.build(this));
  }
Copy the code
  • String. class uses multiple factories, and if it’s a network request, it’s obviously a StreamFactory,
  • Httpurlloader.factory: httpurlLoader.factory: httpurlLoader.factory: httpurlLoader.factory: httpurlLoader.factory: httpurlLoader.factory: httpurlLoader.factory; Look at the loader returned by its build method
public static class StreamFactory implements ModelLoaderFactory<String, InputStream> {
public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
      returnnew StringLoader<>(multiFactory.build(Uri.class, InputStream.class)); } public StringLoader(ModelLoader<Uri, Data> uriLoader) { this.uriLoader = uriLoader; } public ModelLoader<Uri, InputStream> build(MultiModelLoaderFactory multiFactory) {return new HttpUriLoader(multiFactory.build(GlideUrl.class, InputStream.class));
    }
  
Copy the code
  • HttpUrlLoader returns a new layer, glideurl.class, like StringLoader, which was added in 4.10. Glideurl.class is added to the Factory where the glideurl.class was added
 append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())
Copy the code
  • In HttpGlideUrlLoader we find Fetcher
 @Override
  public LoadData<InputStream> buildLoadData(
      @NonNull GlideUrl model, int width, int height, @NonNull Options options) {
    return new LoadData<>(url, new HttpUrlFetcher(url, timeout));
  }
Copy the code
  • In Fecher we find an implementation of network requests and SourceGenerator method parameters.
public Class<InputStream> getDataClass() {
    return InputStream.class;
  }

  @NonNull
  @Override
  public DataSource getDataSource() {
    return DataSource.REMOTE;
  }
  
   /** Indicates data was retrieved from a remote source other than the device. */
  REMOTE,

Copy the code
  • Obviously we’re getting network data, and it fits the bill
  • Then call the loadData method of HttpUrlFetch and internally call loadDataWithRedirects to fetch the network flow
loadData{
 InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());
  callback.onDataReady(result);
 }
 private InputStream loadDataWithRedirects(
      URL url, int redirects, URL lastUrl, Map<String, String> headers) throws IOException {
    urlConnection = connectionFactory.build(url);
    for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
      urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
    }
    urlConnection.setConnectTimeout(timeout);
    urlConnection.setReadTimeout(timeout);
    urlConnection.setUseCaches(false);
    urlConnection.setDoInput(true)}Copy the code

Here we analyzed the Loader to find, and then found out the network request processing class, and then, in turn, the callback, the callback process, can look at the chart, one step, is the callback DecodeJob. OnDataFetcherReady. The next step is to find we started by disk cache class, finally in DecodeJob. Encode method, we see DecodeJob onDataFetcherReady how gradually calls to the method

Call to the disk cache write method

  • After the network request successfully returns the data, it is decoded by decodeFromRetrievedData
  • DecodeFromRetrievedData calls decodeFromData to complete the decoding,
  • Then call notifyEncodeAndRelease
public void onDataFetcherReady(
      Key sourceKey, Object data, DataFetcher<? > fetcher, DataSource dataSource, Key attemptedKey) { ... decodeFromRetrievedData(); . } private voiddecodeFromRetrievedData() {
    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);
    }
    ...
  }
Copy the code
  • NotifyEncodeAndRelease contains two methods, one that returns the EngineJob and the other that writes to the disk cache
  • NotifyComplete is responsible for calling back to EngineJob
  • DeferredEncodeManager encode is responsible for the disk cache writes, here we found diskCacheProvider, options
  • DeferredEncodeManager. Encode is responsible for adding caching
private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) {
    
    Resource<R> result = resource;
    notifyComplete(result, dataSource);

    stage = Stage.ENCODE;
    try {
      if(deferredEncodeManager hasResourceToEncode ()) {/ / deferredEncodeManager. Here is the key method encode (diskCacheProvider, options); } } finally {if(lockedResource ! = null) { lockedResource.unlock(); } } // Call onEncodeComplete outside the finally block so that it's not called if the encode process // throws. onEncodeComplete(); } private void notifyComplete(Resource
      
        resource, DataSource dataSource) { setNotifiedOrThrow(); callback.onResourceReady(resource, dataSource); } void encode(DiskCacheProvider diskCacheProvider, Options options) { GlideTrace.beginSection("DecodeJob.encode"); try { diskCacheProvider .getDiskCache() .put(key, new DataCacheWriter<>(encoder, toEncode, options)); } finally { toEncode.unlock(); GlideTrace.endSection(); }}
      Copy the code

Here through the simple call analysis of the general process, draw a flow chart, and then analyze the cache strategy, step by step analysis of each step of the picture loading process. Through reading the source code, it is found that Gldie will modify some logic in each upgrade, but the general logic is not modified, and its core idea is the same. You can also observe that it covers multiple cases, with so much logic involved in a single call. It’s very well designed.