Glide caching mechanism

Glide caching mechanism

Glide cache mechanism

  • Glide's cache is divided into two types: one is memory cache and the other is hard disk cache. The memory cache includes weak reference cache HashMap and LruCache, and the hard disk cache is DiskLruCache. If there is no URL in the cache and no URL in the cache on the hard disk, the request is to find the image in the cache first. If there is no URL in the cache on the hard disk, and then get the image in the cache through the cache on the network.

Fetching resources from memory cache

In the introduction of the previous blog, we know that the Glide request process only starts loading logic by calling the load method of the Engine at the end, so naturally, the operation of reading cache also starts from the load method of the Engine:

public synchronized <R> LoadStatus load(......){
      
	// Get the key of the resource. There are 8 parameters
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);

    
	EngineResource<?> memoryResource;
    synchronized (this) {
      //Get from memory
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime)
    }

	// If none can be obtained, go to the hard disk cache or network load
	.... 
    }
}
                                        
                                        

From the above lines of code, we divide the steps of memory caching into the following steps to analyze

Step 1: get cache key

// Get the key of the resource. There are 8 parameters
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);

We see that when creating a key, 8 parameters will be passed in, so if we want to get the same picture every time, but the size is different, a new cache key will also be generated. To cache.

  • Glide turns on the memory cache by default. If we want to turn off the memory cache, we can call:

  • //Turn off memory cache
    skipMemoryCache(false) 
    

Step 2: get from the weak reference cache

EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);

The loadFromActiveResources() method was called.

loadFromActiveResources()

  @Nullable
  private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }
    //Go to weak reference activeResources and get it from the HashMap
    EngineResource<?> active = activeResources.get(key);
    //If you can get the resource, call the acquire() method to make the counter + 1
    if (active != null) {
      active.acquire();
    }
    return active;
  }

@Nullable
  synchronized EngineResource<?> get(Key key) {
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    if (activeRef == null) {
      return null;
    }

    EngineResource<?> active = activeRef.get();
    //If this weak reference object is not queried, delete this key
    if (active == null) {
      cleanupActiveReference(activeRef);
    }
    return active;
  }

  synchronized void acquire() {
    if (isRecycled) {
      throw new IllegalStateException("Cannot acquire a recycled resource");
    }
    ++acquired;
  }

This method is to get the weakly referenced objects through the key in the HashMap of weakly referenced active resources. If the key cannot be obtained, the formation may be GC, and the key will be removed from the map; If it is obtained, call the acquire () method to make the counter acquire plus 1, which means it is used. We will summarize the application of the counter at the end.

Step 3: get from memory LruCache

// Get object from lrucache
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);

The loadFromCache method was called

loadFromCache()

  private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }
	
    //Get resources from memory according to key
    EngineResource<?> cached = getEngineResourceFromCache(key);
    //If the resource is obtained, the counter is incremented by 1 and stored in the weak reference cache
    if (cached != null) {
      cached.acquire();
      activeResources.activate(key, cached);
    }
    return cached;
  }

getEngineResourceFromCache()

  private EngineResource<?> getEngineResourceFromCache(Key key) {
    //Call the remove method to get the resource, that is, after obtaining the resource, the resource will be removed from LruCache
    Resource<?> cached = cache.remove(key);

    final EngineResource<?> result;
    if (cached == null) {
      result = null;
    } else if (cached instanceof EngineResource) {
      //Save an object allocation if we've cached an EngineResource (the typical case).
      result = (EngineResource<?>) cached;
    } else {
      result =
          new EngineResource<>(
              cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
    }
    return result;
  }
  • Through the above two methods, we know that to obtain resources from LruCache, we first obtain resources through the remove method, then remove the resources from LruCache, increase its counter by 1, and store the resources in the weak cache.

So far, we have gone through the process of fetching resources from the memory cache: first, get the object from the weak reference. If it exists, the reference count is + 1. If it does not exist, get it from the LruCache. If it does exist, the reference count is + 1, store it in the weak reference, and remove itself from the LruCache.

Memory resources of memory cache

Storage resources must also be stored in the load method of the image loading method Engine. Let's take a look:

Engine.load()

  public synchronized <R> LoadStatus load(
    ..
    // Get key
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);
 // Get object from weak reference
    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    ...
 // Get object from LruCache
    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
 ...
    EngineJob<R> engineJob =
        engineJobFactory.build(
            key,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache);

    DecodeJob<R> decodeJob =
        decodeJobFactory.build(
            glideContext,
            model,
            key,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            onlyRetrieveFromCache,
            options,
            engineJob);
    jobs.put(key, engineJob);

    engineJob.addCallback(cb, callbackExecutor);
    engineJob.start(decodeJob);
 ...
      }

Two key objects will be created here. One is EngineJob, which is a thread pool that maintains coding, resource resolution, network download, etc; The other is DecodeJob, which is used to decode pictures. It inherits Runnable, which is equivalent to a task of EngineJob. Then call EngineJob.. Start (DecodeJob). Let's not talk about the details first. After the method is completed, the onResourceReady () method will be called back

onResourceReady()

  @Override
  public void onResourceReady(Resource<R> resource, DataSource dataSource) {
    synchronized (this) {
      this.resource = resource;
      this.dataSource = dataSource;
    }
    notifyCallbacksOfResult();
  }

notifyCallbacksOfResult()

  @Synthetic
  void notifyCallbacksOfResult() {
    ResourceCallbacksAndExecutors copy;
    Key localKey;
    EngineResource<?> localResource;
    synchronized (this) {
      stateVerifier.throwIfRecycled();
      //Cancelled
      if (isCancelled) {

        resource.recycle();
        release();
        return;
      } else if (cbs.isEmpty()) {
        throw new IllegalStateException("Received a resource without any callbacks to notify");
      } else if (hasResource) {
        throw new IllegalStateException("Already have resource");
      }
      engineResource = engineResourceFactory.build(resource, isCacheable);

      hasResource = true;
      copy = cbs.copy();
      // Reference count + 1
      incrementPendingCallbacks(copy.size() + 1);

      localKey = key;
      localResource = engineResource;
    }
    // put object on weak reference
    listener.onEngineJobComplete(this, localKey, localResource);

	// Traverse all pictures
    for (final ResourceCallbackAndExecutor entry : copy) {
    	// Load resources into imageview, reference count + 1
      entry.executor.execute(new CallResourceReady(entry.cb));
    }
    //Reference count - 1
    decrementPendingCallbacks();
  }

From the above, the notifyCallbacksOfResult() method does the following

  1. Reference count of pictures + 1
  2. Through listener Onenginejobcomplete(), whose callback is engine Onenginejobcomplete(), put ting resources on weak references
  3. Traverse the loaded image. If the loading is successful, the reference count is + 1 and passes CB Onresourceready (engine resource, datasource) callback to target (imageview) to load
  4. Release resources through decrementPendingCallbacks(), reference count - 1

When the reference count is reduced to 0, that is, the image is no longer used, the onResourceReleased() interface will be called

onResourceReleased()

  @Override
  public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
  // Remove from weak references
    activeResources.deactivate(cacheKey);
    if (resource.isCacheable()) {
    //Add to LruCache
      cache.put(cacheKey, resource);
    } else {
     // Recycling resources
      resourceRecycler.recycle(resource);
    }
  }

Disk cache

If you can't get the picture in the memory cache, Glide will judge from two aspects:

  • One is the resource type: whether the picture has been decoded, converted and written to the disk cache before.
  • One is the data source: whether the resources that build this image have been written to the file cache before.

Our hard disk cache; Glide's hard disk strategy can be divided into the following types:

  • DiskCacheStrategy.RESOURCE: only decoded pictures are cached
  • DiskCacheStrategy.DATA: cache only the original picture
  • DiskCacheStrategy.ALL: cache both the original picture and the decoded picture. For remote pictures, cache DATA and RESOURCE; Cache only RESOURCE for local use.
  • DiskCacheStrategy.NONE: do not use hard disk cache
  • DiskCacheStrategy.AUTOMATIC: the default policy, which will use the best policy for local and remote pictures; For downloading network pictures, use DATA; for local pictures, use RESOURCE

When describing the source code of disk cache, attach a flow chart:

As we know from the above, an image is loaded in the class DecodeJob, and this task is executed by the thread pool of EngineJob. Go to the run method and you can see a runWrapped() method:

runWrapped()

private void runwrapped() {
	switch (runReason){
		case INITIALIZE:
			stage = getNextstage(stage.INITIALIZE);
            currentGenerator = getNextGenerator();
            runGenerators();
			break;
		case swITCH_To_sOURCE_SERVICE:
            runGenerators();
			break;
		case DECODE_DATA:
			decodeFromRetrievedData();break;
		default:
		throw new IllegalstateException("Unrecognized run reason: " + runReason);
		}

At first, runReason is initialized to INITIALIZE, so it will take the first case.

  private Stage getNextStage(Stage current) {
    switch (current) {
      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;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
  }

getNextStage is actually to judge the current cache strategy, because our strategy is diskcachestrategy All, so diskcachestrategy Decodecachedresource() is always true, that is, it will parse the decoding process, that is, try to get the decoded picture resources in the disk, so the State is assigned to stage RESOURCE_ Cache (State is a tag). Then go back to the runWrapped () method, continue to execute the call currentGenerator = getNextGenerator(), get the current decoder ResourceCacheGenerator, and then call the runGenerators() method, which is the key:

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;
        }
	if ((stage == stage.FINISHED || iscancelled) && !isstarted){
        notifyFailed();
    }
}

A while loop is maintained, which constantly tries to get the object from DiskLruCache by calling the startNext() method of the current decoder. If it cannot be obtained, it gets the next decoder through the while loop through the getNextstage(stage) and getNextGenerator() methods, and looks for it again when stage = = stage During source, reschedule() will be called to re call the run method of DecodeJob and re run the RunWrapper method.

startNext()

 @Override
  public boolean startNext() {
    List<Key> sourceIds = helper.getCacheKeys();
   ...
    while (modelLoaders == null || !hasNextModelLoader()) {
      resourceClassIndex++;
      if (resourceClassIndex >= resourceClasses.size()) {
        sourceIdIndex++;
        // Since there is no cache, the loop will eventually exit here
        if (sourceIdIndex >= sourceIds.size()) {
          return false;
        }
        resourceClassIndex = 0;
      }

      Key sourceId = sourceIds.get(sourceIdIndex);
	//Get cache key
      currentKey =
          new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoops
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
      // Trying to get data from DiskLruCache
      cacheFile = helper.getDiskCache().get(currentKey);
      if (cacheFile != null) {
        sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      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);
      }
    }
    return started;
  }

Because the cache cannot be obtained for the first time, the while (modelloaders = = null |! Hasnextmodelloader()) loop will run until false is returned.

Similarly, it is the same after getting the DataCacheGenerator. Finally, when stage = = stage It will exit and call the reschedule() method.

  @Override
  public void reschedule() {
    runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
    callback.reschedule(this);
  }

In the reschedule() method, the state of runReason will be changed to runReason SWITCH_TO_SOURCE_SERVICE and call back the run method again, and the runWrapped() method will be called again, but the runReason has become switch at this time_ TO_ SOURCE_ Service, so it will execute the runGenerators() method (the code above has):

At this time, the current decoder currentGenerator is not null, so the startNext() method of SourceGenerator will be called:

  @Override
  public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      // Store to DiskLruCache
      cacheData(data);
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        //Load data
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

First, it will determine whether dataToCache is null. It will be null for the first time, so it can be ignored first. Since sourceCacheGenerator has not been assigned a value, it is null. You can directly look at loaddata fetcher. loadData(); For this method, loaddata () is an interface. It has many implementation methods. Because we download it from the network, we go to httpurlfetcher In loaddata():

loadData()

@override
public void loadData(@NonNull Priority priority,
	@NonNull Datacallback<?super Inputstream> callback){
	long startTime = LogTime.getLogTime( );
	try {
		Inputstream result = loadDatawithRedirects(glideur1.toURL(),0,null,glideur1.getHeaders());
		callback.onDataReady(result);
	}catch (TOException e) {
		if (Log.isLoggable(TAG,Log.DEBUG))
			Log.d(TAG,msg: "Failed to load data for ur1", e);
    }
}

You can see that after it gets the inputStream, it calls back through the onDataReady() method

  @Override
  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;
      // We might be being called back on someone else's thread. Before doing anything, we should
      // reschedule to get back onto Glide's thread.
      cb.reschedule();
    } else {
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }
  }

In sourcegenerator In ondataready(), assign a value to dataToCache; Then call CB The reschedule () method starts all over again, but this time dataToCache is not null, so the cacheData method will be executed (the code logic is shown above):

private void cacheData(object dataTocache){
	long startTime = LogTime.getLogTime();
	try {
		Encoder<object> encoder = helper.getsourceEncoder(dataToCache);
		Datacachewriter<object> writer=new Datacachewriter<>(encoder, dataTocache,helper.getoptions());
		originalKey = new DatacacheKey(loadData.sourceKey,helper.getsignature());
		helper.getDiskcache().put(originalKey,writer);
		if (Log.isLoggable( TAG,Log.VERBOSE))
			Log.v(TAG,msg: "Finished encoding source to cache"
			+ ",key: " + originalKey
			+ ", data: " + dataTocache+ ", encoder: " +encoder
			+ ", duration: " + LogTime.getELapsedMillis(startTime)) ;
	}finally {
		loadData.fetcher.cleanup();
		}
		sourcecacheGenerator =new DataCacheGenerator(collections.singLetonList(loadData.sourceKey), helper,cb: this);
}

You can see that for the parsed data, you can use the helper getDiskCache(). Put() method, which is stored in the hard disk cache of DiskLruCache. And through loaddata fetcher. Clearup () clears the task and assigns sourcecacheGenerator to DataCacheGenerator.
Go back to the startNext method of the SourceGenerator

@override
public boolean startNext() {
	if (dataTocache != null){
		object data = dataTocache;dataTocache = null;
		cacheData(data);
	}
	if (sourceCacheGenerator != null && sourcecacheGenerator.startNext()){
		return true;
	}
	sourcecacheGenerator = null;
	loadData = null:
}

At this time, sourceCacheGenerator is not null, so the startNext() method of DataCacheGenerator will be used.

Before that, the pictures obtained from the network request have been loaded into DiskLruCache, so the requested picture resources have been obtained. Then, after a series of calls, it will call the onResourceReady method of EngineJob. This method, as we said in the memory cache, will store the resources in the weak reference and load the pictures.

Presumably, through the above pile of code, the understanding of hard disk cache will still be blurred, so let's summarize it below.

Hard disk cache summary

To summarize the steps of hard disk caching:

  • 1: Select different cache strategies according to the hard disk strategy. The following steps are based on the hard disk policy as diskcachestrategy The original picture is also cached after all decoding.
  • 2: First call the run method of DecodeJob, and then call the runWrapped method to generate different decoders according to the runReason flag.
  • 3: First obtain the ResourceCaheGenerator to check whether there are decoded picture resources in the hard disk DiskLruCache. If not, get the next decoder DataCacheGenerator and call its startNext method to check whether there are original picture resources in the hard disk DiskLruCache. If not, then call the rescgedule method again to call the run method of DncodeJob again and change the value of runReason.
  • 4: According to runReason, assign the SourceGenerator to the current decoder. For the first time, dataToCache must be null, so a network request will be made to obtain image resources according to the URl, and then call back the run method of DecodeJob
  • Five: also will go to this step, this time because the network has been acquired, so dataToCache is not null, that is, the image resources will be placed in the hard disk cache DiskLruCache, then assign DataCacheGenerator to SourceGenerator, then call SourceGenerator.. startNext is the startNext method of datacachegenerator.
  • 6: Because the data can be obtained from the hard disk at this time, it is different from the execution process of step 3, that is, it will go through a series of steps, call back to the onResourceReady method to display the picture, and put the resources into the weak reference cache.

It will be better understood by combining the following figure again:

Keywords: Android

Added by devang23 on Mon, 03 Jan 2022 05:39:21 +0200