Public number: byte array, hope to help you 😇😇

For Android Developer, many open source libraries are essential knowledge points for development, from the use of ways to implementation principles to source code parsing, which require us to have a certain degree of understanding and application ability. So I’m going to write a series of articles about source code analysis and practice of open source libraries, the initial target is EventBus, ARouter, LeakCanary, Retrofit, Glide, OkHttp, Coil and other seven well-known open source libraries, hope to help you 😇😇

Article Series Navigation:

  • Tripartite library source notes (1) -EventBus source detailed explanation
  • Tripartite library source notes (2) -EventBus itself to implement one
  • Three party library source notes (3) -ARouter source detailed explanation
  • Third party library source notes (4) -ARouter own implementation
  • Three database source notes (5) -LeakCanary source detailed explanation
  • Tripartite Library source note (6) -LeakCanary Read on
  • Tripartite library source notes (7) -Retrofit source detailed explanation
  • Tripartite library source notes (8) -Retrofit in combination with LiveData
  • Three party library source notes (9) -Glide source detailed explanation
  • Tripartite library source notes (10) -Glide you may not know the knowledge point
  • Three party library source notes (11) -OkHttp source details
  • Tripartite library source notes (12) -OkHttp/Retrofit development debugger
  • Third party library source notes (13) – may be the first network Coil source analysis article

Glide source code is a little complex, if you want to elaborate to explain, then write a ten articles also include not complete 😂😂 so I think a different way of thinking to see the source: To small points to divide, each small point only contains Glide to achieve a function or purpose involved in the process, in order to simplify the difficulty of understanding, through the integration of a number of small function points to control the Glide big implementation direction

This article is based on the latest version of Glide currently available

dependencies {
    implementation 'com. Making. Bumptech. Glide: glide: 4.11.0'
    kapt 'com. Making. Bumptech. Glide: the compiler: 4.11.0'
}
Copy the code

An overview,

Before you start looking at Glide source code, you need to have some basic understanding of Glide

Glide’s cache mechanism is divided into memory cache and disk cache two levels. By default, Glide will automatically cache the loaded images, which can be divided into memory cache and disk cache, and the cache logic adopts LruCache algorithm. By default, Glide values a network image in the following order:

  1. When launching a request to load an image, it first checks to see if there is a matching image in ActiveResources. If so, it takes the value directly, otherwise it goes to the next step. ActiveResources is used to store in memory the image resource that is currently in use (for example, an ImageView is displaying the image), and ActiveResources holds a reference to the image through a weak reference
  2. Check whether the MemoryCache contains a matching image. If so, take the value directly, otherwise go to the next step. MemoryCache uses the Lru algorithm to cache in memory image resources that have been used but are not currently in use
  3. Check whether there are images matching the conditions in DiskCache. If yes, decode the value. Otherwise, go to the next step. DiskCache also uses the Lru algorithm to cache images that have been loaded on the local disk
  4. Networking request image. After the image is loaded, the image is cached to disk and memory, namely DiskCache, ActiveResources, and MemoryCache, for subsequent reuse

So Glide’s MemoryCache is divided into ActiveResources and MemoryCache

In addition, Glide will eventually be cached to disk can be divided into two types of images, one is the original image, one is the original image after various compression, cropping, transformation and other conversion operations. Glide’s DiskCacheStrategy is divided into the following five categories to determine how to save these two types of images on disk

Disk caching Policy Cache Policy
DiskCacheStrategy.NONE Nothing is cached
DiskCacheStrategy.ALL Cache both the original image and the converted image
DiskCacheStrategy.DATA Only the original image is cached
DiskCacheStrategy.RESOURCE Only the converted images are cached
DiskCacheStrategy.AUTOMATIC Glide automatically selects which caching policy to use based on the image resource type (default option)

, a special caching policy is DiskCacheStrategy. The AUTOMATIC, the strategy will be depending on the type of image to be loaded using the best caching strategies. If you load remote images, only the original images are stored, not the converted images, because downloading remote images is much more expensive than adjusting data already on the disk. If you load a local image, only the converted image is stored, because even if you need to generate another image of another size or type, it’s easy to get back the original image

Since disk space is limited, AUTOMATIC is a middle-of-the-road choice that measures both the size of the disk space and the cost of obtaining images

How to monitor the life cycle

The ImageView is mounted on the Activity or Fragment container. When the container is in the background or has been finished, the loading of the image should be cancelled or stopped. Otherwise, you are wasting valuable system and network resources, and may even have memory leaks or NPE problems. The obvious question is, how does Glide determine whether a container is still active?

Similar to the implementation of Lifecycle in the Jetpack component, Glide indirectly obtains the Lifecycle state of the container through a UI-less Fragment. Lifecycle implementation ideas can be found in my source code explanation article:Jetpack (1) -Lifecycle source parsing from source

Glide implementation lifecycle listening involves the following classes:

  1. LifecycleListener
  2. Lifecycle
  3. ActivityFragmentLifecycle
  4. ApplicationLifecycle
  5. SupportRequestManagerFragment

First, LifecycleListener defines three event notification callbacks to notify the container of its active state (foreground, background, or exit). Lifecycle is used to register and remove the LifecycleListener

public interface LifecycleListener {
  void onStart(a);
  void onStop(a);
  void onDestroy(a);
}

public interface Lifecycle {
  void addListener(@NonNull LifecycleListener listener);
  void removeListener(@NonNull LifecycleListener listener);
}
Copy the code

For a container instance, for example, during the lifetime of an Activity, the Activity may load more than one image successively. Accordingly, multiple background tasks to load the image need to be started successively, and each background task needs to be notified when the Activity’s lifecycle state changes. This entire notice process corresponding ActivityFragmentLifecycle this class

ActivityFragmentLifecycle isStarted and isDestroyed two Boolean variables to mark the Activity of the current active state, and provides the ability to save and inform more LifecycleListener

class ActivityFragmentLifecycle implements Lifecycle {
  private final Set<LifecycleListener> lifecycleListeners =
      Collections.newSetFromMap(new WeakHashMap<LifecycleListener, Boolean>());
  private boolean isStarted;
  private boolean isDestroyed;

  @Override
  public void addListener(@NonNull LifecycleListener listener) {
    lifecycleListeners.add(listener);

    if (isDestroyed) {
      listener.onDestroy();
    } else if (isStarted) {
      listener.onStart();
    } else{ listener.onStop(); }}@Override
  public void removeListener(@NonNull LifecycleListener listener) {
    lifecycleListeners.remove(listener);
  }

  void onStart(a) {
    isStarted = true;
    for(LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onStart(); }}void onStop(a) {
    isStarted = false;
    for(LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onStop(); }}void onDestroy(a) {
    isDestroyed = true;
    for(LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onDestroy(); }}}Copy the code

ActivityFragmentLifecycle used in SupportRequestManagerFragment the fragments to use (omitted part of the code). As you can see, in the three fragments lifecycle callback event, will inform ActivityFragmentLifecycle accordingly. So, whether or not the ImageView is the carrier of the Activity or fragments, we can to inject a UI SupportRequestManagerFragment, to monitor the carrier changes in the active state in the entire life cycle

public class SupportRequestManagerFragment extends Fragment {
    
  private static final String TAG = "SupportRMFragment";
    
  private final ActivityFragmentLifecycle lifecycle;

  public SupportRequestManagerFragment(a) {
    this(new ActivityFragmentLifecycle());
  }

  @VisibleForTesting
  @SuppressLint("ValidFragment")
  public SupportRequestManagerFragment(@NonNull ActivityFragmentLifecycle lifecycle) {
    this.lifecycle = lifecycle;
  }

  @NonNull
  ActivityFragmentLifecycle getGlideLifecycle(a) {
    return lifecycle;
  }

  @Override
  public void onStart(a) {
    super.onStart();
    lifecycle.onStart();
  }

  @Override
  public void onStop(a) {
    super.onStop();
    lifecycle.onStop();
  }

  @Override
  public void onDestroy(a) {
    super.onDestroy();
    lifecycle.onDestroy();
    unregisterFragmentWithRoot();
  }

  @Override
  public String toString(a) {
    return super.toString() + "{parent=" + getParentFragmentUsingHint() + "}"; }}Copy the code

Glide cannot do Lifecycle listening in two special cases where the Lifecycle implementation class is ApplicationLifecycle and is always onStart by default:

  • The Context passed to Glide is of the Application type. Applications do not have life cycle events in the usual sense
  • Load the image in the child thread. At this point the developer may want to get to the Bitmap object directly
class ApplicationLifecycle implements Lifecycle {
  @Override
  public void addListener(@NonNull LifecycleListener listener) {
    listener.onStart();
  }

  @Override
  public void removeListener(@NonNull LifecycleListener listener) {
    // Do nothing.}}Copy the code

3. How to inject fragments

Now know Glide through SupportRequestManagerFragment got life cycle events, So how does SupportRequestManagerFragment mounted to the Activity or fragments?

By looking for reference, to locate is done in RequestManagerRetriever getSupportRequestManagerFragment method SupportRequestManagerFragment injection

public class RequestManagerRetriever implements Handler.Callback {
    
  @NonNull
  private SupportRequestManagerFragment getSupportRequestManagerFragment(
      @NonNull final FragmentManager fm, @Nullable Fragment parentHint, boolean isParentVisible) {
    / / by the TAG whether in FragmentManager already contains SupportRequestManagerFragment
    SupportRequestManagerFragment current =
        (SupportRequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
    if (current == null) {
      / / the current SupportRequestManagerFragment null shows that hasn't been injected
      / / then build a SupportRequestManagerFragment instance and added to the FragmentManager
      current = pendingSupportRequestManagerFragments.get(fm);
      if (current == null) {
        current = new SupportRequestManagerFragment();
        current.setParentFragmentHint(parentHint);
        if(isParentVisible) { current.getGlideLifecycle().onStart(); } pendingSupportRequestManagerFragments.put(fm, current); fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss(); handler.obtainMessage(ID_REMOVE_SUPPORT_FRAGMENT_MANAGER, fm).sendToTarget(); }}returncurrent; }}Copy the code

What is the exact timing of the injection?

When we use Glide to load an image, it’s often something as unpretentious as the one shown below, a single line of code, with tons of work behind the scenes

Glide.with(FragmentActivity).load(url).into(ImageView)
Copy the code

When you call Glide. With (FragmentActivity), the final call will be to the RequestManagerRetriever’s get(FragmentActivity) method, In internal call supportFragmentGet method complete SupportRequestManagerFragment injection, and eventually return to a RequestManager object, The RequestManager stores all image loading tasks started with this FragmentActivity

public class RequestManagerRetriever implements Handler.Callback {
    
  @NonNull
  public RequestManager get(@NonNull FragmentActivity activity) {
    if (Util.isOnBackgroundThread()) {
      // For background threads, then ApplicationLifecycle is used
      return get(activity.getApplicationContext());
    } else {
      assertNotDestroyed(activity);
      FragmentManager fm = activity.getSupportFragmentManager();
      return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity)); }}@NonNull
  private RequestManager supportFragmentGet(
      @NonNull Context context,
      @NonNull FragmentManager fm,
      @Nullable Fragment parentHint,
      boolean isParentVisible) {
    / / finish SupportRequestManagerFragment injection operation here
    SupportRequestManagerFragment current =
        getSupportRequestManagerFragment(fm, parentHint, isParentVisible);
    RequestManager requestManager = current.getRequestManager();
    if (requestManager == null) {
      // TODO(b/27524013): Factor out this Glide.get() call.
      Glide glide = Glide.get(context);
      requestManager =
          factory.build(
              glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
      current.setRequestManager(requestManager);
    }
    returnrequestManager; }}Copy the code

So, when we call Glide. With (FragmentActivity) method, where you have already completed the SupportRequestManagerFragment injection

The RequestManagerRetriever has a GET method overload that contains several inbound parameter types

  1. Context
  2. androidx.fragment.app.FragmentActivity
  3. android.app.Activity
  4. androidx.fragment.app.Fragment
  5. Android.app. Fragment (deprecated)
  6. View

The logic of these GET methods can be summarized as:

  1. If the external thread is called through a child thread, then use the Application thread. In this case, you do not need to inject the Fragment, and use the ApplicationLifecycle directly. By default, the external thread is always active
  2. If Application is passed in from the outside, the steps are the same as above
  3. If the incoming View is not associated with the Activity (for example, the View contains a Context of type ServiceContext), then the steps are the same as above
  4. In addition to the above circumstance, can eventually through external to the incoming parameters associated to find the Activity or fragments, and eventually to inject RequestManagerFragment or SupportRequestManagerFragment

RequestManagerFragment function and SupportRequestManagerFragment, but is, here is no longer here

For example, the get (@ NonNull Context Context) will be based on the caller’s thread type and Context belongs to determine how to inject SupportRequestManagerFragment, resulting in different RequestManager. If no injection SupportRequestManagerFragment, then use RequestManager objects belong to the Application level of RequestManager globally unique

  /** The top application level RequestManager. */
  private volatile RequestManager applicationManager;

  @NonNull
  public RequestManager get(@NonNull Context context) {
    if (context == null) {
      throw new IllegalArgumentException("You cannot start a load on a null Context");
    } else if(Util.isOnMainThread() && ! (contextinstanceof Application)) {
      // Call on the main thread where context is not Application
        
      if (context instanceof FragmentActivity) {
        return get((FragmentActivity) context);
      } else if (context instanceof Activity) {
        return get((Activity) context);
      } else if (context instanceof ContextWrapper
          // Only unwrap a ContextWrapper if the baseContext has a non-null application context.
          // Context#createPackageContext may return a Context without an Application instance,
          // in which case a ContextWrapper may be used to attach one.&& ((ContextWrapper) context).getBaseContext().getApplicationContext() ! =null) {
        returnget(((ContextWrapper) context).getBaseContext()); }}// Call in the child thread or context is Application
    return getApplicationManager(context);
  }

  @NonNull
  private RequestManager getApplicationManager(@NonNull Context context) {
    // Either an application context or we're on a background thread.
    if (applicationManager == null) {
      synchronized (this) {
        if (applicationManager == null) {
          // Normally pause/resume is taken care of by the fragment we add to the fragment or
          // activity. However, in this case since the manager attached to the application will not
          // receive lifecycle events, we must force the manager to start resumed using
          // ApplicationLifecycle.

          // TODO(b/27524013): Factor out this Glide.get() call.
          Glide glide = Glide.get(context.getApplicationContext());
          applicationManager =
              factory.build(
                  glide,
                  new ApplicationLifecycle(),
                  newEmptyRequestManagerTreeNode(), context.getApplicationContext()); }}}return applicationManager;
  }
Copy the code

4. How to start the task of loading images

We’ve seen how Glide monitors Activity lifecycle changes. How does Glide initiate the task of loading images?

Mentioned above, when we call the Glide. With (FragmentActivity), will complete SupportRequestManagerFragment injection operation. For the same Activity instance, it is injected only once during its single lifecycle. From supportFragmentGet method can also see that every SupportRequestManagerFragment contains a RequestManager instance

  public class RequestManagerRetriever implements Handler.Callback {
    
  @NonNull
  private RequestManager supportFragmentGet(
      @NonNull Context context,
      @NonNull FragmentManager fm,
      @Nullable Fragment parentHint,
      boolean isParentVisible) {
    / / finish SupportRequestManagerFragment injection operation here
    SupportRequestManagerFragment current =
        getSupportRequestManagerFragment(fm, parentHint, isParentVisible);
    RequestManager requestManager = current.getRequestManager();
    if (requestManager == null) {
      / / if requestManager null will be generated and set into the SupportRequestManagerFragment
      // TODO(b/27524013): Factor out this Glide.get() call.
      Glide glide = Glide.get(context);
      requestManager =
          factory.build(
              glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
      current.setRequestManager(requestManager);
    }
    returnrequestManager; }}Copy the code

The RequestManager class is where you start and manage all image-loading tasks before and after an Activity starts. When we fully call the Into method Glide. With (FragmentActivity).load(URL).into(ImageView), we build a Request object that represents the currently loaded task. And pass the task to the RequestManager to start tracking the task

  @NonNull
  public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) {...return into(
        glideContext.buildImageViewTarget(view, transcodeClass),
        /*targetListener=*/ null,
        requestOptions,
        Executors.mainThreadExecutor());
  }

  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()");
    }
    // Build a Request object that represents the loading taskRequest request = buildRequest(target, targetListener, options, callbackExecutor); ... requestManager. The clear (target); target.setRequest(request);// Start tracking the task by passing the request to the requestManager
    requestManager.track(target, request);
    return target;
  }
Copy the code

The key is the requestManager.track(Target, Request) code, which is the initiation point of the task

public class RequestManager implements ComponentCallbacks2.LifecycleListener.ModelTypes<RequestBuilder<Drawable>> {  

    // Store all tasks
	@GuardedBy("this")
  	private final RequestTracker requestTracker;

  	@GuardedBy("this")
  	private final TargetTracker targetTracker = new TargetTracker();  

  	synchronized void track(@NonNullTarget<? > target,@NonNull Request request) {
    	targetTracker.track(target);
        // Run the taskrequestTracker.runRequest(request); }}Copy the code

RequestTracker is used to store all requests, that is, all image-loading tasks, and provides methods to start, pause, and restart all tasks. The external controls whether the task is currently allowed to start by changing the value of the isPaused variable. The runRequest method uses isPaused to determine whether to start the task immediately or to store the task in pendingRequests

public class RequestTracker {
  private static final String TAG = "RequestTracker";

  private final Set<Request> requests =
      Collections.newSetFromMap(new WeakHashMap<Request, Boolean>());

  @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
  private final List<Request> pendingRequests = new ArrayList<>();

  private boolean isPaused;

  /** Starts tracking the given request. */
  public void runRequest(@NonNull Request request) {
    // Save the task first
    requests.add(request);
    // Start the task if it is not paused, otherwise save it to the to-do list
    if(! isPaused) { request.begin(); }else {
      request.clear();
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Paused, delaying request"); } pendingRequests.add(request); }}/** Stops any in progress requests. */
  public void pauseRequests(a) {
    isPaused = true;
    for (Request request : Util.getSnapshot(requests)) {
      if (request.isRunning()) {
        // Avoid clearing parts of requests that may have completed (thumbnails) to avoid blinking
        // in the UI, while still making sure that any in progress parts of requests are immediately
        // stopped.request.pause(); pendingRequests.add(request); }}}/** Restarts failed requests and cancels and restarts in progress requests. */
  public void restartRequests(a) {
    for (Request request : Util.getSnapshot(requests)) {
      if(! request.isComplete() && ! request.isCleared()) { request.clear();if(! isPaused) { request.begin(); }else {
          // Ensure the request will be restarted in onResume.pendingRequests.add(request); }}}} ···}Copy the code

When SupportRequestManagerFragment in onStop (), can use RequestTracker, transfer will isPaused set to true. In addition, when SupportRequestManagerFragment execution to onDestroy (), means that the Activity has been finish, A callback notification is made to the RequestManager’s onDestroy() method, where tasks are cleaned up and various registration events are unregistered

  @Override
  public synchronized void onDestroy(a) {
    targetTracker.onDestroy();
    for(Target<? > target : targetTracker.getAll()) { clear(target); } targetTracker.clear(); requestTracker.clearRequests(); lifecycle.removeListener(this);
    lifecycle.removeListener(connectivityMonitor);
    mainHandler.removeCallbacks(addSelfToLifecycle);
    glide.unregisterRequestManager(this);
  }
Copy the code

Five, the specific process of loading pictures

Request is an interface that represents each image loading Request. It contains several implementation classes, such as SingleRequest. SingleRequest’s begin() method checks the current task status to prevent repeated loading, then gets the target width and height or ImageView width and height, and then determines whether the placeholders need to be shown first

  public final class SingleRequest<R> implements Request.SizeReadyCallback.ResourceCallback {
    
  @Override
  public void begin(a) {
    synchronized (requestLock) {
      assertNotCallingCallbacks();
      stateVerifier.throwIfRecycled();
      startTime = LogTime.getLogTime();
      if (model == null) {
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
          width = overrideWidth;
          height = overrideHeight;
        }
        // Only log at more verbose log levels if the user has set a fallback drawable, because
        // fallback Drawables indicate the user expects null models occasionally.
        int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
        // If model is null, it indicates that no image source address is passed in
        onLoadFailed(new GlideException("Received null model"), logLevel);
        return;
      }
	
      // Prevent repeated startup while tasks are running
      if (status == Status.RUNNING) {
        throw new IllegalArgumentException("Cannot restart a running request");
      }

      if (status == Status.COMPLETE) {
        // Return the loaded image resource
        onResourceReady(resource, DataSource.MEMORY_CACHE);
        return;
      }

      // Restarts for requests that are neither complete nor running can be treated as new requests
      // and can run again from the beginning.
	
      // First get the target width and height or ImageView width and load as needed
      status = Status.WAITING_FOR_SIZE;
      if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        onSizeReady(overrideWidth, overrideHeight);
      } else {
        target.getSize(this);
      }

      if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
          && canNotifyStatusChanged()) {
        // Pass out the placeholder first
        target.onLoadStarted(getPlaceholderDrawable());
      }
      if (IS_VERBOSE_LOGGABLE) {
        logV("finished run method in "+ LogTime.getElapsedMillis(startTime)); }}}}Copy the code

As you can see, the above logic does not cover the specific image loading logic, because this process needs to be done after the target width and height are obtained. If a specific width and height value is passed in from the outside, the external value will prevail, otherwise the target (such as ImageView) will prevail. Only after the width and height are obtained will the load actually start, all in order to achieve on-demand loading and avoid memory waste

So, the focus is on the onSizeReady method. All the configuration information (image address, width, height, priority, cache allowed, etc.) is transferred to the Engine’s load method to load the image

  private volatile Engine engine;  

  /** A callback method that should never be invoked directly. */
  @Override
  public void onSizeReady(int width, int height) {
    stateVerifier.throwIfRecycled();
    synchronized (requestLock) {
      if (IS_VERBOSE_LOGGABLE) {
        logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
      }
      if(status ! = Status.WAITING_FOR_SIZE) {return;
      }
      status = Status.RUNNING;
	
      // Scale
      float sizeMultiplier = requestOptions.getSizeMultiplier();
      this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
      this.height = maybeApplySizeMultiplier(height, sizeMultiplier);

      if (IS_VERBOSE_LOGGABLE) {
        logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
      }
      // Start loading images
      loadStatus =
          engine.load(
              glideContext,
              model,
              requestOptions.getSignature(),
              this.width,
              this.height,
              requestOptions.getResourceClass(),
              transcodeClass,
              priority,
              requestOptions.getDiskCacheStrategy(),
              requestOptions.getTransformations(),
              requestOptions.isTransformationRequired(),
              requestOptions.isScaleOnlyOrNoTransform(),
              requestOptions.getOptions(),
              requestOptions.isMemoryCacheable(),
              requestOptions.getUseUnlimitedSourceGeneratorsPool(),
              requestOptions.getUseAnimationPool(),
              requestOptions.getOnlyRetrieveFromCache(),
              this,
              callbackExecutor);

      // This is a hack that's only useful for testing right now where loads complete synchronously
      // even though under any executor running on any thread but the main thread, the load would
      // have completed asynchronously.
      if(status ! = Status.RUNNING) { loadStatus =null;
      }
      if (IS_VERBOSE_LOGGABLE) {
        logV("finished onSizeReady in "+ LogTime.getElapsedMillis(startTime)); }}}Copy the code

The configuration information passed to the Engine also contains a ResourceCallback object, called SingleRequest itself, because SingleRequest implements the ResourceCallback interface. From the names of the methods included in ResourceCallback, you can see that the Engine uses these methods to call back the results when the image loading succeeded or failed

public interface ResourceCallback {

  void onResourceReady(Resource
        resource, DataSource dataSource);

  void onLoadFailed(GlideException e);

  Object getLock(a);
}
Copy the code

The load method generates a unique key for the request. The key is used to determine whether the image can be reused. Then, the value is obtained from the memory cache based on this key. Or add a callback to an existing task

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;
	
    // Generate a unique identification key for this request. This key is the basis for determining whether the image reuse can be realizedEngineKey key = keyFactory.buildKey( model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource<? > memoryResource;synchronized (this) {
      // Select value from memory cache first
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

      if (memoryResource == null) {
        // If the target resource does not exist in memory, start a new task to load it, or add a callback to an existing task
        returnwaitForExistingOrStartNewJob( glideContext, model, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, options, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache, cb, callbackExecutor, key, startTime); }}// Avoid calling back while holding the engine lock, doing so makes it easier for callers to
    // deadlock.
    cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
    return null;
  }
Copy the code

For the request of the load network image, waitForExistingOrStartNewJob method for the image corresponds to the request through the network or is the process of loading a local disk file, if the target image has not been downloaded to the request for network, if has been to the local cache before to a disk loading. The loadFromMemory method corresponds to the process of trying to find the target image in memory, because the target image may have been loaded into memory before, and is used to try to overuse the image resource in memory

Here’s an example of loading a network image, from network request to disk cache to memory cache

1. Network request

Glide. With (Context).load(Any) is a multi-overloaded method that supports multiple input types such as Integer, String, Uri, File, etc. And finally we may get Bitmap, Drawable, GifDrawable and other results. So, how does Glide distinguish between our different entry requests? And how do you handle different request types?

Glide class contains a Registry variable, which acts as a registry that stores the processing logic for a particular input parameter type, and the resulting value type expected from that input parameter type

    registry
        .append(Uri.class, InputStream.class, new UriLoader.StreamFactory(contentResolver))
        .append(
            Uri.class,
            ParcelFileDescriptor.class,
            new UriLoader.FileDescriptorFactory(contentResolver))
        .append(
            Uri.class,
            AssetFileDescriptor.class,
            new UriLoader.AssetFileDescriptorFactory(contentResolver))
        .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())
        .append(byte[].class, ByteBuffer.class, new ByteArrayLoader.ByteBufferFactory())
        .append(byte[].class, InputStream.class, new ByteArrayLoader.StreamFactory())
        .append(Uri.class, Uri.class, UnitModelLoader.Factory.<Uri>getInstance())
        .append(Drawable.class, Drawable.class, UnitModelLoader.Factory.<Drawable>getInstance())
        .append(Drawable.class, Drawable.class, new UnitDrawableDecoder())
        /* Transcoders */
        .register(Bitmap.class, BitmapDrawable.class, new BitmapDrawableTranscoder(resources))
        .register(Bitmap.class, byte[].class, bitmapBytesTranscoder)
        .register(
            Drawable.class,
            byte[].class,
            new DrawableBytesTranscoder(
                bitmapPool, bitmapBytesTranscoder, gifDrawableBytesTranscoder))
        .register(GifDrawable.class, byte[].class, gifDrawableBytesTranscoder);
Copy the code

For example, one of the most common ways we request images from the network is through the image Url, which corresponds to the following configuration. The GlideUrl corresponds to the ImageUrl we passed in, and the InputStream is the resource InputStream that we want to obtain from the network according to the Url. The HttpGlideUrlLoader is used to implement the process of converting the ImageUrl to the InputStream

	append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())
Copy the code

The HttpGlideUrlLoader will pass the ImageUrl to the HttpUrlFetcher to make the specific network request

public class HttpGlideUrlLoader implements ModelLoader<GlideUrl.InputStream> {
 
    @Override
  	public LoadData<InputStream> buildLoadData(
      	@NonNull GlideUrl model, int width, int height, @NonNull Options options) {
    	// GlideUrls memoize parsed URLs so caching them saves a few object instantiations and time
    	// spent parsing urls.
    	GlideUrl url = model;
    	if(modelCache ! =null) {
      		url = modelCache.get(model, 0.0);
      			if (url == null) {
        	modelCache.put(model, 0.0, model); url = model; }}int timeout = options.get(TIMEOUT);
    	return new LoadData<>(url, newHttpUrlFetcher(url, timeout)); }}Copy the code

HttpUrlFetcher in loadDataWithRedirects method through HttpURLConnection to request the image, and finally through DataCallback to get the image InputStream InputStream object out through. In addition, the loadDataWithRedirects method directs itself by calling the loadDataWithRedirects loop. You are not allowed to repeat redirects to the same Url up to five times, otherwise you go through the failed process

public class HttpUrlFetcher implements DataFetcher<InputStream> {
 
    private static final int MAXIMUM_REDIRECTS = 5;
    
  @Override
  public void loadData(
      @NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
    long startTime = LogTime.getLogTime();
    try {
      InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0.null, glideUrl.getHeaders());
      callback.onDataReady(result);
    } catch (IOException e) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Failed to load data for url", e);
      }
      callback.onLoadFailed(e);
    } finally {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Finished http url fetcher fetch in "+ LogTime.getElapsedMillis(startTime)); }}}private InputStream loadDataWithRedirects(
      URL url, int redirects, URL lastUrl, Map<String, String> headers) throws IOException {
    if (redirects >= MAXIMUM_REDIRECTS) {
       // When the total number of redirects reaches five, the process fails
      throw new HttpException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");
    } else {
      // Comparing the URLs using .equals performs additional network I/O and is generally broken.
      // See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
      try {
        if(lastUrl ! =null && url.toURI().equals(lastUrl.toURI())) {
          // Loop redirection to the same Url, go through the failure process
          throw new HttpException("In re-direct loop"); }}catch (URISyntaxException e) {
        // Do nothing, this is best effort.} } urlConnection = connectionFactory.build(url); ... the stream = urlConnection. GetInputStream ();if (isCancelled) {
      return null;
    }
    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

2. Disk cache

Look back Engine class waitForExistingOrStartNewJob method. When it is determined that there is no target image in the current memory cache, EngineJob and DecodeJob will be started for disk file loading or network request loading

private <R> LoadStatus waitForExistingOrStartNewJob(
      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,
      EngineKey key,
      long startTime) { EngineJob<? > current = jobs.get(key, onlyRetrieveFromCache);if(current ! =null) {
      // If the same request task has already been started, add a callback to it
      current.addCallback(cb, callbackExecutor);
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Added to existing load", startTime, key);
      }
      return new LoadStatus(cb, current);
    }

    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);
    / / start decodeJob
    engineJob.start(decodeJob);

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

We’ll focus on the DecodeJob class here. As mentioned earlier, Glide can be divided into two types of images that are cached on disk, one is the original image, the other is the original image after various conversion operations, such as compression, cropping, transformation, etc. This behavior is determined by the diskCacheStrategy parameter

            Glide.with(context).load(imageUrl)
                .diskCacheStrategy(DiskCacheStrategy.DATA)
                .into(imageView)
Copy the code

If we are using DiskCacheStrategy.data, we will cache the original image and try to load the locally cached original image at the time of loading. This property affects both write and read operations. The DecodeJob will select the corresponding DataFetcherGenerator for processing according to our cache configuration, so there are three possible source types for the final image:

  1. Reuse converted picture resources. Corresponding to ResourceCacheGenerator, if the cache does not match, go to the next step
  2. Reuse the original image resources. Corresponding to DataCacheGenerator, the next step is performed when the cache does not hit
  3. No local cached resource meets the requirements, and a new load is required (networking request). Corresponding to the SourceGenerator
  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

For example, the main logic of DataCacheGenerator is the startNext() method, which takes the original image from DiskCache and gets the cacheFile cacheFile and the corresponding processor modelLoaders, ModelLoaders contains all implementers that can perform the conversion operation (e.g., File to Drawable, File to Bitmap, etc.), and returns true if the cache File and the corresponding converter are determined. When the DataCacheGenerator succeeds in loading the Target data, it calls back to the DecodeJob’s onDataFetcherReady method, eventually storing the Target data in ActiveResources and notifying all targets

  @Override
  public boolean startNext(a) {
    while (modelLoaders == null| |! hasNextModelLoader()) { sourceIdIndex++;if (sourceIdIndex >= cacheKeys.size()) {
        return false;
      }

      Key sourceId = cacheKeys.get(sourceIdIndex);
      // PMD.AvoidInstantiatingObjectsInLoops The loop iterates a limited number of times
      // and the actions it performs are much more expensive than a single allocation.
      @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
      // Take the value from the disk cache
      cacheFile = helper.getDiskCache().get(originalKey);
      if(cacheFile ! =null) {
        this.sourceKey = sourceId;
        // Get all the data type converters
        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 DataCacheGenerator represents the case where the target image is fetched from the local disk cache, and the logic for requesting the network image and writing it to the local disk depends on the SourceGenerator

The SourceGenerator calls the onDataReadyInternal method after the image has been successfully loaded via HttpUrlFetcher. If disk caching is not allowed on this request, the DecodeJob’s onDataFetcherReady method is called directly to complete the process, consistent with the DataCacheGenerator. If disk caching is allowed, the startNext() method is called to reschedule(), the disk file is written to the cacheData method, and a DataCacheGenerator is constructed. The value is then taken from the disk by DataCacheGenerator

  void onDataReadyInternal(LoadData
        loadData, Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if(data ! =null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      // To allow disk caching, cache data in the dataToCache variable
      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); }}@Override
  public boolean startNext(a) {
    if(dataToCache ! =null) {
      //dataToCache is not null, indicating that it is time to cache images to disk
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }

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

  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());
      // Write to the disk cache
      helper.getDiskCache().put(originalKey, writer);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(
            TAG,
            "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, this);
  }
Copy the code

Glide’s disk caching algorithm specifically corresponds to the DiskLruCache class, which is Glide based on JakeWharton’s DiskLruCache open source library modified, here is not too much detail

No matter how the DecodeJob gets the images, it will eventually call the onEngineJobComplete method of the Engine class, which will cache the loaded images into memory. This is the source of the in-memory cache data

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

3. Memory cache

Let’s look at the memory caching mechanism. Glide’s MemoryCache is divided into two levels: ActiveResources and MemoryCache. The operation of MemoryCache corresponds to the loadFromMemory method of Engine class

  • Value from ActiveResources based on key, and call if obtainedacquire()Method to increase the number of references to the resource by one, otherwise go to the next step
  • Calls based on the value of the key from MemoryCache, if it is obtainedacquire()Method increments the number of references to the resource by one and simultaneously removes the resource from MemoryCache and stores it in ActiveResources, returning null if no value is obtained
  private final ActiveResources activeResources;

  private final MemoryCache cache;

  // Try to load the image resource from memory
  @Nullable
  privateEngineResource<? > loadFromMemory( EngineKey key,boolean isMemoryCacheable, long startTime) {
    if(! isMemoryCacheable) {// If the memory cache is not allowed, return it directly
      return null;
    }
	
    // Load from ActiveResourcesEngineResource<? > active = loadFromActiveResources(key);if(active ! =null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return active;
    }

    // Load from MemoryCacheEngineResource<? > cached = loadFromCache(key);if(cached ! =null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return cached;
    }

    return null;
  }

  @Nullable
  privateEngineResource<? > loadFromActiveResources(Key key) { EngineResource<? > active = activeResources.get(key);if(active ! =null) {
      active.acquire();
    }

    return active;
  }

  privateEngineResource<? > loadFromCache(Key key) { EngineResource<? > cached = getEngineResourceFromCache(key);if(cached ! =null) {
      cached.acquire();
      activeResources.activate(key, cached);
    }
    return cached;
  }

  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

ActiveResources is a weak reference to all currently in use image resources. As we know, if an object has only weak references and is no longer strongly referenced, then when GC occurs, the references held in the weak reference are directly nulled and the weak reference object itself is stored in the associated ReferenceQueue

When a new image is loaded and used, and memory caching is currently allowed, the image resource is saved to the activeEngineResources via the activate method. When the reference count of an image resource becomes 0, it indicates that the resource is no longer in external use. In this case, the deactivate method will be used to remove it from the activeEngineResources to eliminate the reference to the resource. The resource is also stored in MemoryCache if memory caching is currently allowed

final class ActiveResources {
 
  final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
 
  private finalReferenceQueue<EngineResource<? >> resourceReferenceQueue =new ReferenceQueue<>();
  
  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(); }}synchronized void deactivate(Key key) {
    ResourceWeakReference removed = activeEngineResources.remove(key);
    if(removed ! =null) { removed.reset(); }}@Synthetic
  void cleanupActiveReference(@NonNull ResourceWeakReference ref) {
    synchronized (this) {
      activeEngineResources.remove(ref.key);

      if(! ref.isCacheable || ref.resource ==null) {
        return; } } EngineResource<? > newResource =new EngineResource<>(
            ref.resource, /*isMemoryCacheable=*/ true./*isRecyclable=*/ false, ref.key, listener); listener.onResourceReleased(ref.key, newResource); }}// Corresponds to the Engine class
 @Override
 public void onResourceReleased(Key cacheKey, EngineResource
        resource) {
    // Remove the image resource from activeResources
    activeResources.deactivate(cacheKey);
    if (resource.isMemoryCacheable()) {
      // If memory caching is allowed, the image resources are stored in MemoryCache
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource, /*forceNextFrame=*/ false); }}Copy the code

The default implementation of MemoryCache corresponds to the LruResourceCache class. As the name suggests, MemoryCache uses an Lru algorithm, which caches images based on the maximum MemoryCache size passed in from outside. The logic itself is relatively simple, but I don’t need to go into much detail

LruResourceCache contains a ResourceRemovedListener object that is used to notify the Engine when an image object has been removed from the memory cache. The Engine recycles the image resource

public class LruResourceCache extends LruCache<Key.Resource<? >>implements MemoryCache {
    
  @Override
  public void setResourceRemovedListener(@NonNull ResourceRemovedListener listener) {
    this.listener = listener;
  }

  @Override
  protected void onItemEvicted(@NonNull Key key, @NullableResource<? > item) {
    if(listener ! =null&& item ! =null) { listener.onResourceRemoved(item); }}}Copy the code

So let’s summarize the logic and relationship between ActiveResources and MemoryCache

  1. ActiveResources holds a weak reference to an image resource that is currently in use. When an image is loaded successfully and still in use, ActiveResources holds a reference to it. When images are no longer used, they are removed from ActiveResources and stored in MemoryCache
  2. MemoryCache uses the Lrc algorithm to cache image resources in memory, only those that are not currently in use. When an image cached in the MemoryCache is reused externally, the image is removed from the MemoryCache and restored in ActiveResources
  3. In ActiveResources, the image stored in ActiveResources is a resource that is currently in a strong reference state. So Glide’s memory cache value from ActiveResources will not increase the current used memory. As the system memory size is limited, MemoryCache uses THE Lrc algorithm to save memory as much as possible and to maximize the probability that the images will be reused can be retained
  4. Glide divides the MemoryCache into ActiveResources and MemoryCache, rather than putting them all into MemoryCache, thus avoiding the error of removing currently active image resources from the queue. In addition, ActiveResources also circularly determines whether the saved image resources are no longer used externally, so that MemoryCache can be updated in a timely manner and the utilization and accuracy of MemoryCache can be improved

Vi. Memory cleaning mechanism

Glide’s memoryCache mechanism is to reuse image resources as much as possible to avoid frequent disk read and write and memory read and write. MemoryCache, bitmapPool and arrayPool exist for this purpose, but on the other hand, memoryCache also causes a part of memory space to be occupied all the time. The available memory space of the system may be insufficient. When our application is back to the background, if the available memory space of the system is insufficient, then the system will clean up some background processes according to the priority, in order to free up memory space for the foreground process, in order to improve the priority of the application in the background, we need to take the initiative to reduce our memory usage

Fortunately, Glide also considers this situation and provides an automatic cleaning mechanism for cache memory. Glide’s initializeGlide method registers a ComponentCallbacks to Application by default to receive notifications of memory state changes sent by the system

  @GuardedBy("Glide.class")
  @SuppressWarnings("deprecation")
  private static void initializeGlide(
      @NonNull Context context,
      @NonNull GlideBuilder builder,
      @Nullable GeneratedAppGlideModule annotationGeneratedModule) { Context applicationContext = context.getApplicationContext(); ... applicationContext. RegisterComponentCallbacks (glide); Glide.glide = glide; }Copy the code

The corresponding ComponentCallbacks implementation class is Glide class itself, and its related method implementation corresponds to the following two

  @Override
  public void onTrimMemory(int level) {
    trimMemory(level);
  }

  @Override
  public void onLowMemory(a) {
    clearMemory();
  }
Copy the code

These two methods automatically trigger the cleanup of memoryCache, bitmapPool, and arrayPool

  public void trimMemory(int level) {
    // Engine asserts this anyway when removing resources, fail faster and consistently
    Util.assertMainThread();
    // Request managers need to be trimmed before the caches and pools, in order for the latter to
    // have the most benefit.
    for (RequestManager manager : managers) {
      manager.onTrimMemory(level);
    }
    // memory cache needs to be trimmed before bitmap pool to trim re-pooled Bitmaps too. See #687.
    memoryCache.trimMemory(level);
    bitmapPool.trimMemory(level);
    arrayPool.trimMemory(level);
  }

  public void clearMemory(a) {
    // Engine asserts this anyway when removing resources, fail faster and consistently
    Util.assertMainThread();
    // memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687.
    memoryCache.clearMemory();
    bitmapPool.clearMemory();
    arrayPool.clearMemory();
  }
Copy the code

Include several thread pools

To conclude, Glide is a total of seven thread pools, if I’m not missing anything. Here I am referring to the thread pool not only refers to the concept of ThreadPoolExecutor class, it is the Java. Util. Concurrent. Any implementation of the Executor interface classes

Of these, the first four thread pools can be answered by the constructor parameter of the EngineJob class

class EngineJob<R> implements DecodeJob.Callback<R>, Poolable { EngineJob( GlideExecutor diskCacheExecutor, GlideExecutor sourceExecutor, GlideExecutor sourceUnlimitedExecutor, GlideExecutor animationExecutor, EngineJobListener engineJobListener, ResourceListener resourceListener, Pools.Pool<EngineJob<? >> pool) {this( diskCacheExecutor, sourceExecutor, sourceUnlimitedExecutor, animationExecutor, engineJobListener, resourceListener, pool, DEFAULT_FACTORY); }}Copy the code

Its uses are:

  1. DiskCacheExecutor. Used to load the disk cache
  2. SourceExecutor. Used to perform operations that do not load the local disk cache, such as loading images based on the specified URI or ImageUrl
  3. SourceUnlimitedExecutor. With sourceExecutor
  4. AnimationExecutor. The official annotation is used to load giFs

The four thread pools are created using the GlideExecutor class.

  1. DiskCacheExecutor. The number of core threads and the maximum number of threads are both 1, and the thread timeout is 0 seconds. Because diskCacheExecutor reads and writes disk files, both the core thread count and the maximum thread count are one, so that only one thread is active at all times when the thread pool is started, ensuring file read and write order and avoiding locking operations
  2. SourceExecutor. The number of core threads and the maximum number of threads are determined by the number of cpus on the device, which is at least 4 threads with a thread timeout of 0 seconds. The thread count setting limits Glide to a maximum of four network load image requests
  3. SourceUnlimitedExecutor. The number of core threads is 0, the maximum number of threads is integer.max_value, and the timeout time is 10 seconds. When the thread is idle, it will be reclaimed immediately. The purpose of sourceUnlimitedExecutor is to cope with the need to process a large number of load image requests simultaneously, allowing nearly unlimited number of new threads to handle each request, which may improve the timeliness of sourceExecutor. However, it can also reduce efficiency due to multi-threaded competition, and it is also easy to OOM
  4. AnimationExecutor. If the number of cpus on the device is greater than 4, the number of core threads and the maximum number of threads are set to 2; otherwise, the number is set to 1. The thread timeout is 0 seconds

All four thread pools are used for the EngineJob class. DiskCacheExecutor is only used for disk caching and will be used as long as disk caching is allowed for this request. And three other thread pool in my point of view is used to load a local file or a network request pictures, if useUnlimitedSourceGeneratorPool is true, just use sourceUnlimitedExecutor, Otherwise, if useAnimationPool is true, animationExecutor is used, otherwise sourceExecutor is used

UseUnlimitedSourceGeneratorPool well understand the meaning of, that is, in order to control the maximum number of threads concurrent requests at the same time, but the meaning of distinguish useAnimationPool I don’t understand, understand the classmates to solve the trouble

  public synchronized void start(DecodeJob<R> decodeJob) {
    this.decodeJob = decodeJob;
    GlideExecutor executor =
        decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();
    executor.execute(decodeJob);
  }

  private GlideExecutor getActiveSourceExecutor(a) {
    return useUnlimitedSourceGeneratorPool
        ? sourceUnlimitedExecutor
        : (useAnimationPool ? animationExecutor : sourceExecutor);
  }
Copy the code

The fifth thread pool is located in the ActiveResources class. This thread pool is used to continuously determine values in the ReferenceQueue to cache image resources that are currently no longer in external use into MemoryCache

 ActiveResources(boolean isActiveResourceRetentionAllowed) {
    this(
        isActiveResourceRetentionAllowed,
        java.util.concurrent.Executors.newSingleThreadExecutor(
            new ThreadFactory() {
              @Override
              public Thread newThread(@NonNull final Runnable r) {
                return new Thread(
                    new Runnable() {
                      @Override
                      public void run(a) { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); r.run(); }},"glide-active-resources"); }})); }Copy the code

The remaining two thread pools are in the Executors class

  1. MAIN_THREAD_EXECUTOR. Use Handler to switch to the main thread to update the UI when the image has been loaded

  2. DIRECT_EXECUTOR. This is an empty implementation that executes Runnable on the original thread, and is used when we want to get images directly instead of updating the UI, such as Glide. With (this).load(URL).submit()

public final class Executors {
  private Executors(a) {
    // Utility class.
  }

  private static final Executor MAIN_THREAD_EXECUTOR =
      new Executor() {
        private final Handler handler = new Handler(Looper.getMainLooper());

        @Override
        public void execute(@NonNull Runnable command) { handler.post(command); }};private static final Executor DIRECT_EXECUTOR =
      new Executor() {
        @Override
        public void execute(@NonNull Runnable command) { command.run(); }};/** Posts executions to the main thread. */
  public static Executor mainThreadExecutor(a) {
    return MAIN_THREAD_EXECUTOR;
  }

  /** Immediately calls {@link Runnable#run()} on the current thread. */
  public static Executor directExecutor(a) {
    return DIRECT_EXECUTOR;
  }

  @VisibleForTesting
  public static void shutdownAndAwaitTermination(ExecutorService pool) {
    long shutdownSeconds = 5;
    pool.shutdownNow();
    try {
      if(! pool.awaitTermination(shutdownSeconds, TimeUnit.SECONDS)) { pool.shutdownNow();if(! pool.awaitTermination(shutdownSeconds, TimeUnit.SECONDS)) {throw new RuntimeException("Failed to shutdown"); }}}catch (InterruptedException ie) {
      pool.shutdownNow();
      Thread.currentThread().interrupt();
      throw newRuntimeException(ie); }}}Copy the code

Custom network request library

By default, Glide loads images over the network using HttpURLConnection, which is relatively raw and inefficient compared to our usual OkHttp. Glide also provides a Registry class that allows external definitions to implement specific request logic

For example, if you want to request images via OkHttp, you can rely on Glide’s official support library:

dependencies {
    implementation "Com. Making. Bumptech. Glide: okhttp3 - integration: 4.11.0"
}
Copy the code

Glide automatically assigns network type requests to its internal OkHttp as long as okHttp3-integration is in place. Because it contains an OkHttpLibraryGlideModule class that declares the @glidemodule annotation, it can be resolved by Glide at run time, The GlideUrl type load request is then passed to OkHttpUrlLoader for processing

@GlideModule
public final class OkHttpLibraryGlideModule extends LibraryGlideModule {
  @Override
  public void registerComponents(
      @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
    registry.replace(GlideUrl.class, InputStream.class, newOkHttpUrlLoader.Factory()); }}Copy the code

We can also copy the code from okHttp3-Integration and pass in our own OkHttpUrlLoader in our custom AppGlideModule class

@GlideModule
class MyAppGlideModule : AppGlideModule() {

    override fun isManifestParsingEnabled(a): Boolean {
        return false
    }

    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        val okHttClient = OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .readTimeout(15, TimeUnit.SECONDS)
            .eventListener(object : EventListener() {
                override fun callStart(call: okhttp3.Call) {
                    Log.e("TAG"."CallStart." + call.request().url().toString())
                }
            }).build()
        registry.replace(
            GlideUrl::class.java, InputStream::class.java,
            OkHttpUrlLoader.Factory(okHttClient)
        )
    }

}
Copy the code