Let’s first look at two aspects of picture loading performance optimization in Glide official documentation:

  • Picture decoding speed
  • Resource pressure from decoding images

The main steps are as follows:

  1. Automatic and intelligent downsampling and caching to minimize storage overhead and decoding times;
  2. Aggressive resource reuse, such as byte arrays and bitmaps, to minimize the impact of expensive garbage collection and heap fragmentation;
  3. Deep lifecycle integration to ensure that only active Fragment and Activity requests are processed first, and to help applications free resources if necessary to avoid being killed in the background.

Glide cache mechanism

Official document has detailed instructions muyangmin. Making. IO/glide – docs -…

Multi-level cache logic

Multistage cache

  1. Active Resources – Is there another View showing this image right now?
  2. Memory cache – Has the image been loaded recently and still exists in Memory?
  3. Resource Type – Has this image been decoded, converted, and written to disk cache before?
  4. Data Source – Was the resource used to build this image previously written to the file cache?

The first two steps check if the image is in memory, and if so, return the image directly. The last two steps check that the picture is on disk so that it can be returned quickly but asynchronously. If all four steps fail to find the image, Glide returns to the original resource to retrieve the data (original file, Uri, Url, etc.).

Caching strategies

Memory caching strategy

Active and in-memory resources are cached in memory. Active resources are stored by weak reference of HashMap. Memory cache resources are stored by LRU cache.

Disk cache policy types (see DiskCacheStrategy) :
  1. Diskcachestrategy. DATA: DATA written to a disk is the original DATA that has not been modified by the loading process.
  2. DiskCacheStrategy. RESOURCE: the decoding, after the transformation of RESOURCE to disk;
  3. Diskcachestrategy. ALL: The remote resource (URL resource) writes both the original resource and the transformed resource. Local file resources will only be written to the resources after decoding transformation;
  4. Diskcachestrategy. NONE: No data is written to the disk.
  5. DiskCacheStrategy. AUTOMATIC: it will try to use the best strategy for local and remote images. When you load remote data (for example, downloaded from a URL),AUTOMATICPolicies only store raw data that has not been modified (e.g. transformed) by your loading process, because downloading remote data is much more expensive than adjusting data that already exists on disk. For local data,AUTOMATICThe strategy is to store only the thumbnails that have been transformed, because it’s easy to retrieve the original data even if you need to generate another image of a different size or type.

The cache keys

Glide builds different keys for different cache scenarios, and active resource and memory caches use slightly different keys than disk resource caches to accommodate memory, options, such as options that affect Bitmap configurations, or other parameters that are only used when decoding. Take the memory key value as an example. If the image size of the requested resource changes, the image cannot match the key in the cache. You need to remove the disk or obtain the image again from the network to change the image. Let’s look at the construction of the relevant Key;

EngineKey(
      Object model,
      Key signature,
      int width,
      intheight, Map<Class<? >, Transformation<? >> transformations, Class<? > resourceClass, Class<? > transcodeClass, Options options) ResourceCacheKey( ArrayPool arrayPool, Key sourceKey, Key signature,int width,
      intheight, Transformation<? > appliedTransformation, Class<? > decodedResourceClass, Options Options DataCacheKey(Key sourceKey, Key signature)Copy the code

In Glide V4, all cache keys contain at least two elements:

  1. Request model (File, Url, Url) to load. If you use a custom Model, it needs to be implemented correctlyhashCode() 和 equals()
  2. An optional oneThe signature(Signature)

In addition, the cache keys for steps 1-3(Active Resource, memory cache, Resource disk cache) also contain some additional data, including:

  1. Width and height
  2. optionalTransformation
  3. Any additional additionsOption (Options)
  4. The data type requested (Bitmap, GIF, or other)

Modify the default cache configuration

Glide also provides a way to modify the default cache configuration items;

// Change the size of the memory cache configuration (of course, you can customize your cache)
@GlideModule
public class YourAppGlideModule extends AppGlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    int memoryCacheSizeBytes = 1024 * 1024 * 20; // 20mb
    builder.setMemoryCache(newLruResourceCache(memoryCacheSizeBytes)); }}Copy the code

And configure the cache policy using the loading of an image:

Glide.with(fragment)
  .load(url)
  .diskCacheStrategy(DiskCacheStrategy.ALL)
  .into(imageView);
Copy the code

Glide cache source code analysis

The normal operation for loading an image is as follows:

RequestBuilder

Into (imageView) (scaleType = imageView) (scaleType = imageView) (scaleType = imageView) (scaleType = imageView) (scaleType = imageView) (scaleType = imageView) (scaleType = imageView)

Then enter:

into(
        glideContext.buildImageViewTarget(view, transcodeClass),
        /*targetListener=*/ null,
        requestOptions,
        Executors.mainThreadExecutor())
Copy the code

Then create a request via buildRequest and proceed

private <Y extends Target<TranscodeType>> Y into(
      @NonNull Y target,
      @NullableRequestListener<TranscodeType> targetListener, BaseRequestOptions<? > options, Executor callbackExecutor) {
    Preconditions.checkNotNull(target);
    if(! isModelSet) {throw new IllegalArgumentException("You must call #load() before calling #into()");
    }
    //1. Create a request
    Request request = buildRequest(target, targetListener, options, callbackExecutor);

    Request previous = target.getRequest();
    if(request.isEquivalentTo(previous) && ! isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {// If two requests are the same, if the previous request fails, the request will be repeated
    // If the last request was already running, it will continue to run without interrupting it.
      if(! Preconditions.checkNotNull(previous).isRunning()) {// If the same request did not run, run the previous request directly.
       // This can optimize some operations such as setting up station maintenance, recording, obtaining image information and so on in a request
        previous.begin();
      }
      return target;
    }

    requestManager.clear(target);
    target.setRequest(request);
   // Start executing the request
    requestManager.track(target, request);

    return target;
  }
Copy the code

SingleRequest

Finally through requestManager. Track (target, request) call RequestTracker. RunRequest, through SingleRequest. The begin, SingleRequest. OnSizeReady method into the Engine. The load method;

Let’s focus on the engine.load method

public <R> LoadStatus load(
      GlideContext glideContext,
      Object model,
      Key signature,
      int width,
      intheight, Class<? > resourceClass, Class<R> transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map<Class<? >, Transformation<? >> transformations,boolean isTransformationRequired,
      boolean isScaleOnlyOrNoTransform,
      Options options,
      boolean isMemoryCacheable,
      boolean useUnlimitedSourceExecutorPool,
      boolean useAnimationPool,
      boolean onlyRetrieveFromCache,
      ResourceCallback cb,
      Executor callbackExecutor) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

   // First, generate the raw cache keyEngineKey key = keyFactory.buildKey( model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource<? > memoryResource;synchronized (this) {
     // Step 2, read the resource from memory
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

      if (memoryResource == null) {
      // Step 3, start a new task: read the required resources from disk or network.
        return waitForExistingOrStartNewJob(
            glideContext,
            model,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            options,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache,
            cb,
            callbackExecutor,
            key,
            startTime);
      }
    }
    cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
    return null;
  }
Copy the code

Step 1: Generate the cached key, where a key is created based on the model, signature, width, transform, and original resource, transformed resource, and options described above. This is the original key, and subsequent execution to disk will generate a new key based on the original key. Step 2: Try to fetch the resource file from memory, which is divided into two parts: active resource and memory cache resource.

  @Nullable
  privateEngineResource<? > loadFromMemory( EngineKey key,boolean isMemoryCacheable, long startTime) {
    if(! isMemoryCacheable) {return null;
    }
    // Activity resourcesEngineResource<? > active = loadFromActiveResources(key);if(active ! =null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return active;
    }
    // Memory cache resourcesEngineResource<? > cached = loadFromCache(key);if(cached ! =null) {
      return cached;
    }

    return null;
  }
Copy the code

Activity resources The ultimate implementation of activity resources is through Activity Resources. Internally maintains a Map

, that is, a weakly referenced hashMap, which stores the active resource in the weak reference of the hashMap.
,>

@VisibleForTesting final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();

  synchronized void activate(Key key, EngineResource
        resource) {
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
    ResourceWeakReference removed = activeEngineResources.put(key, toPut);
    if(removed ! =null) { removed.reset(); }}Copy the code

Memory cache

  privateEngineResource<? > getEngineResourceFromCache(Key key) { Resource<? > cached = cache.remove(key);finalEngineResource<? > 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;
  }
Copy the code

The key implementation is the cache defined by MemoryCache. The implementation class of MemoryCache is LruResourceCache. LruResourceCache integrates LruCache, which implements LRU algorithm to maintain memory cache data.

Step 3: Start a new task: read the required resources from disk or network. Through waitForExistingOrStartNewJob into engineJob. Start (decodeJob); DecodeJob runs -> runWrapped -> runGenerators. DecodeJob runs -> runWrapped -> runGenerators

private void runGenerators(a) {
    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();
    }
  }
Copy the code

Including currentGenerator. StartNext () method to actually perform loading, we’ll look at currentGenerator what are the specific implementation class.

If the current stage is returned according to the cache policy, the generator will be generated according to the current stage.

  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); }}Copy the code

The three producers are created based on the current phase

  • The resource cache file corresponds to ResourceCacheGenerator
  • No transformed metadata cache file corresponds to the DataCacheGenerator
  • Source data SourceGenerator corresponding to data (network or local file)

The logic is to search the local resource cache file for the required resource, if not, search the metadata cache, if not, go to the source data to load, and write the current data to the disk cache.

private DataFetcherGenerator getNextGenerator(a) {
    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
ResourceCacheGenerator
 public boolean startNext(a) {··· omit code currentKey =new ResourceCacheKey( // NOPMD AvoidInstantiatingObjectsInLoops
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
    // Determine whether the disk cache exists according to 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;
  }
Copy the code

The default implementation is DiskLruCache, which reads the resource from DiskCache. After the resource is encoded, it is saved to disk.

DataCacheGenerator

DataCacheGenerator. StartNext logic and ResourceCacheGenerator basic same. First, create a DataCacheKey, which differs from the ResourceCacheGenerator Key generation method. The second step is to try to get the metadata cache from the third party through DiskCache and load the data according to modelLoader

SourceGenerator

Start by trying to load the source data, mainly through the DataFetcher, which selects the appropriate one to call based on the data source type.

    private void startNextLoad(finalLoadData<? > toStart) {
    loadData.fetcher.loadData(
        helper.getPriority(),
        new DataCallback<Object>() {
          @Override
          public void onDataReady(@Nullable Object data) {
            if(isCurrentRequest(toStart)) { onDataReadyInternal(toStart, data); }}@Override
          public void onLoadFailed(@NonNull Exception e) {
            if(isCurrentRequest(toStart)) { onLoadFailedInternal(toStart, e); }}}); }Copy the code

The DataFetcher implementation class is shown below.

Let’s go to HttpUrlFetcher and see how this works. Here we see the loadData withRedirects method inside the loadData method to get into the actual file network data reading.

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);
    final int statusCode = urlConnection.getResponseCode();
    if (isHttpOk(statusCode)) {
      return getStreamForSuccessfulRequest(urlConnection);
    } else if (isHttpRedirect(statusCode)) {
      String redirectUrlString = urlConnection.getHeaderField("Location");
      if (TextUtils.isEmpty(redirectUrlString)) {
        throw new HttpException("Received empty or null redirect url");
      }
      URL redirectUrl = new URL(url, redirectUrlString);
      // Closing the stream specifically is required to avoid leaking ResponseBodys in addition
      // to disconnecting the url connection below. See #2352.
      cleanup();
      return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
    } else if (statusCode == INVALID_STATUS_CODE) {
      throw new HttpException(statusCode);
    } else {
      throw newHttpException(urlConnection.getResponseMessage(), statusCode); }}Copy the code

OnDataFetcherReady calls DecodeJob and loadPath. load. Finally, the ResourceTranscoder is converted into BitmapResource, BitmapDrawableResource, FileResource and so on.