Glide is a popular image loading framework on Android, so we won’t introduce its basic usage here. You can learn the basics by looking at its official documentation. Here, we give a basic example of Glide’s use to see what happens in the process:

Glide.with(fragment).load(myUrl).into(imageView);
Copy the code

Although the above code is simple, the entire execution process involves many classes, and the process is relatively complex. To illustrate this process more clearly, we have broken Glide’s picture load into the following sections based on the time of call:

  1. with()Method execution process
  2. load()Method execution process
  3. into()Method execution process
    1. Phase 1: OnDecodeJobThe process of
    2. Phase 2: The process of opening the network flow
    3. Phase 3: Converts the input stream toDrawableThe process of
    4. Stage 4: willDrawableShow toImageViewThe process of

Following the example above, the into() process is broken down into four stages: with(), load(), and into().

Now we will introduce the operations in each process according to the above division.

1. Execution process of the with() method

1.1 The process of instantiating the Glide of a singleton

It gets a RequestManager instance when Glide’s with() method is called. With () has multiple overloaded methods, and we can use activities or fragments to get Glide instances. They all end up calling the following method to do the final operation:

public static RequestManager with(Context context) {
    return getRetriever(context).get(context);
}
Copy the code

Within the getRetriever() method we first use the Get () method of Glide to get a singleton Glide instance, and then get a RequestManagerRetriever from that Glide instance:

private static RequestManagerRetriever getRetriever(Context context) {
    return Glide.get(context).getRequestManagerRetriever();
}
Copy the code

This calls Glide’s get() method, which eventually instantiates a singleton Glide instance by calling the initializeGlide() method. We have introduced this approach in previous articles. It is used to retrieve GlideModule from annotations and the Manifest, and to customize Glide according to the methods in each GlideModule:

Glide Series -1: Common Configuration and Principle of preheating and Glide

The following method requires passing in an instance of GlideBuilder. This is clearly an application of the Builder pattern, and we can use its approach to personalize Glide:

private static void initializeGlide(Context context, GlideBuilder builder) {

    / /... Various operations, omitted

    // Assign to a static singleton instance
    Glide.glide = glide;
}
Copy the code

Finally, Glide instance is built by the Build () method of GlideBuilder. It calls the Glide constructor directly to create Glide. This constructor registers the mapping of various types of image resources and their loading classes to Glide, which you can read the source code for.

1.2 Glide life cycle management

Another important part of the execution of the with() method is Glide’s lifecycle management. We need to manage Glide’s life cycle because the Fragment or Activity life cycle may have ended while we were loading images.

Glide also handles this very cleverly, using fragments without a UI to manage Glide’s life cycle. This is also a very common lifecycle management approach, used by frameworks such as RxPermission. You can use the following example to see how it works:

Example code: Manage onActivityResult() with a Fragment

In the With () method, after we call the Get () method in the RequestManagerRetriever retriever, the various overloaded methods of get() are called based on the Context type.

  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)) {
      if (context instanceof FragmentActivity) {
        return get((FragmentActivity) context);
      } else if (context instanceof Activity) {
        return get((Activity) context);
      } else if (context instanceof ContextWrapper) {
        returnget(((ContextWrapper) context).getBaseContext()); }}return getApplicationManager(context);
  }
Copy the code

Let’s take an Activity as an example. The RequestManager is retrieved using the Application Context when currently in the background thread, as shown in the following method. Otherwise, the RequestManager is managed using a UI-free Fragment:

  public RequestManager get(@NonNull Activity activity) {
    if (Util.isOnBackgroundThread()) {
      return get(activity.getApplicationContext());
    } else {
      assertNotDestroyed(activity);
      android.app.FragmentManager fm = activity.getFragmentManager();
      return fragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity)); }}Copy the code

The fragmentGet() method is then called. Here we get the Lifecycle object from the RequestManagerFragment via getGlideLifecycle(). Lifecycle objects provide a set of methods for the Fragment Lifecycle. They will be called back in the Fragment’s various lifecycle methods.

  private RequestManager fragmentGet(Context context, FragmentManager fm, 
    Fragment parentHint, boolean isParentVisible) {
    RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
    RequestManager requestManager = current.getRequestManager();
    if (requestManager == null) {
      Glide glide = Glide.get(context);
      requestManager =
          factory.build(
              glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
      current.setRequestManager(requestManager);
    }
    return requestManager;
  }
Copy the code

We then pass that Lifecycle into the RequestManager, using two methods in the RequestManager that will listen on Lifecycle, Listen on the Fragment lifecycle:

  public void onStart(a) {
    resumeRequests();
    targetTracker.onStart();
  }

  public void onStop(a) {
    pauseRequests();
    targetTracker.onStop();
  }
Copy the code

1.3 summary

After the above analysis, we can summarize the execution of Glide’s with() method using the following flowchart:

2. Execution process of load() method

2.1 Load () process

Once we have the RequestManager we can use it to call the load() method. In our example, we pass in a URL object. The load() method is also overloaded, and we can pass in multiple resource types including Bitmap, Drawable, Uri, and String. The following method is called in the example to get a RequestBuilder object, which is clearly an application of the Builder pattern. We can use RequestBuilder’s other methods to continue building image load requests. You can check its source code to see what Kind of build methods Glide has for us:

  public RequestBuilder<TranscodeType> load(@Nullable String string) {
    return loadGeneric(string);
  }
Copy the code

It’s worth mentioning that there is a apply() method in the RequestBuilder constructor, which is defined as follows. As you can see from the method definition below, we can configure the current image load request by specifying RequestOptions for the RequestBuilder. For example, specify the disk cache policy, specify the placeholder map, specify the image to be displayed when an image load error occurs, and so on. So how do we get RequestOptions? The RequestOptions class in Glide 4.8.0 provides us with a series of static methods that we can use to get an instance of RequestOptions:

  public RequestBuilder<TranscodeType> apply(RequestOptions requestOptions) {
    Preconditions.checkNotNull(requestOptions);
    this.requestOptions = getMutableOptions().apply(requestOptions);
    return this;
  }
Copy the code

Coming back, we can continue to trace the load() method. In fact, no matter which overloaded method of load() we use, the following method will eventually be called. The logic is simple, just assign our image resource information to a local variable in the RequestBuilder. How images are loaded and displayed is handled in the into() method.

  public RequestBuilder<TranscodeType> load(@Nullable String string) {
    return loadGeneric(string);
  }

  private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {
    this.model = model;
    isModelSet = true;
    return this;
  }
Copy the code

2.2 summary

So, we can summarize the execution of Glide’s load() method as follows. The process of getting a RequestBuilder using a RequestManger:

3. Execution of the into() method

Considering that into() method has a long process and involves many analogies, we divide it into four stages according to the process of image loading.

The first stage is to start the DecodeJob process. DecodeJob is responsible for loading image resources from the cache or from the original data source, image transformation and transcoding, Glide picture loading process is the core. DecodeJob inherits Runnable and puts it into the thread pool when the image is actually loaded. In this phase we will focus on building a DecodeJob from the RequestBuilder and starting the DecodeJob task. The process of building a DecodeJob and dropping it into a thread pool.

The second stage is the process of turning on the network flow. This phase loads the image data from the data source based on our image resources. In our example, images are loaded from the network and an InputStream is obtained by default.

The third stage is the process of converting the input stream to a Drawable. Once you’ve got the InputStream, call BitmapFactory’s decodeStream() method to get a Drawable from the InputStream.

The fourth stage is the process of displaying the Drawable onto the ImageView.

3.1 Stage 1: Start the DecodeJob process

3.1.1 Process analysis

We continue our analysis along the into() method.

The into() method is also defined in the RequestBuilder and is also overloaded. Whichever overloaded method we call encapsulates the object used to display the image as a Target. Target is used to manage the life cycle of the object used to display images. When we load an image into ImageView, we’ll eventually call the following buildTarget() method to wrap our ImageView into a ViewTarget, and then call into() to do the rest:

  public <Z> ViewTarget<ImageView, Z> buildTarget(ImageView view, Class<Z> clazz) {
    if (Bitmap.class.equals(clazz)) {
      return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view);
    } else if (Drawable.class.isAssignableFrom(clazz)) {
      return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view);
    } else {
      throw new IllegalArgumentException(
          "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)"); }}private <Y extends Target<TranscodeType>> Y into(Y target, RequestListener
       
         targetListener, RequestOptions options)
        {

    options = options.autoClone();
    Request request = buildRequest(target, targetListener, options); / / 1

    Request previous = target.getRequest();
    if(request.isEquivalentTo(previous) && ! isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) { request.recycle();if(! Preconditions.checkNotNull(previous).isRunning()) { previous.begin(); }return target;
    }

    requestManager.clear(target);
    target.setRequest(request);
    requestManager.track(target, request); / / 2

    return target;
  }
Copy the code

The following method is eventually called at 1 of the into() method above to build a request object. (We ignore the parameters here and just show the logic for building the request). In short, this method will return mainRequest or errorRequestCoordinator depending on whether we have called the RequestBuilder error() method to set the image to display when an image loading error occurs. Since we did not set this parameter, mainRequest will be returned directly.

  private Request buildRequestRecursive(/* Various parameters */) {

    ErrorRequestCoordinator errorRequestCoordinator = null;
    if(errorBuilder ! =null) {
      errorRequestCoordinator = new ErrorRequestCoordinator(parentCoordinator);
      parentCoordinator = errorRequestCoordinator;
    }

    Request mainRequest = buildThumbnailRequestRecursive(/* Various parameters */); / / 1

    if (errorRequestCoordinator == null) {
      return mainRequest;
    }

    / /... slightly

    Request errorRequest = errorBuilder.buildRequestRecursive(/* Various parameters */);
    errorRequestCoordinator.setRequests(mainRequest, errorRequest);
    return errorRequestCoordinator;
  }
Copy the code

The above is based on whether to set the image to display when the load fails to determine the request object returned. If you’ve used Glide, remember that in addition to setting up the image to load when it fails, we also load a small image, called the Thumbnail. So, at method 1 above, the RequestBuilder’s thumbnail() method is called to determine whether the request to return the thumbnail is a real image request, depending on the Settings. Again, since we haven’t set this method, we end up calling the following method to build the final image load request.

  private Request obtainRequest(/* Various parameters */) {
    return SingleRequest.obtain(/* Various parameters */);
  }
Copy the code

The SingleRequest obtain() method attempts to retrieve a request from the pool of requests, instantiates a SingleRequest when it does not exist, and then initializes the request by calling its init() method. The request Pool here uses the Pool-related API from Android’s Support V4 package. It is designed to build array-based request pooling, and you can refer to the documentation and source code for details.

  public static <R> SingleRequest<R> obtain(/* Various parameters */) {
    SingleRequest<R> request = (SingleRequest<R>) POOL.acquire();
    if (request == null) {
      request = new SingleRequest<>();
    }
    request.init(/* Various parameters */);
    return request;
  }
Copy the code

After getting the request, we use the track() method of the RequestManager:

  void track(@NonNull Target
        target, @NonNull Request request) {
    targetTracker.track(target);
    requestTracker.runRequest(request);
  }
Copy the code

There are two main functions of this method:

  1. callTargetTrackertrack()Method pairs currentTargetLife cycle management;
  2. callRequestTrackerrunRequest()Method manages the current request and is used directly when Glide is not in the suspended stateRequestbegin()Method to open a request.

Here is the begin() method of SingeleRequest. It determines which method should be called based on the current loading state. Since our image loading process may have been stopped for some unexpected reason, we need to restore the image based on the previous state when we restart. For our first load, it goes directly to the onSizeReady() method 1 below:

  public void begin(a) {
    if (model == null) {
      if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        width = overrideWidth;
        height = overrideHeight;
      }
      int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
      onLoadFailed(new GlideException("Received null model"), logLevel);
      return;
    }

    if (status == Status.RUNNING) {
      throw new IllegalArgumentException("Cannot restart a running request");
    }

    // If we restart after we are done (usually with methods such as notifyDataSetChanged(),
    // Launch the same request in the same target or view), we can use the resource and size we retrieved last time
    // And skip getting the new size. So, if you want to reload the image because the View size has changed
    // Clear the View or Target before starting a new load.
    if (status == Status.COMPLETE) {
      onResourceReady(resource, DataSource.MEMORY_CACHE);
      return;
    }

    status = Status.WAITING_FOR_SIZE;
    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
      onSizeReady(overrideWidth, overrideHeight); / / 1
    } else {
      target.getSize(this);
    }

    if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
        && canNotifyStatusChanged()) {
      target.onLoadStarted(getPlaceholderDrawable()); / / 2}}Copy the code

Here is the onSizeReady() method, which determines whether it is currently in status.waiting_for_size and then changes the state to status.running and calls engine’s load() method. Obviously, after changing the state and continuing back to the above method, Target’s onLoadStarted() method is called at 2. Target’s first life cycle is triggered.

  public void onSizeReady(int width, int height) {
    if(status ! = Status.WAITING_FOR_SIZE) {return;
    }
    status = Status.RUNNING;

    float sizeMultiplier = requestOptions.getSizeMultiplier();
    this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
    this.height = maybeApplySizeMultiplier(height, sizeMultiplier);

    loadStatus = engine.load(/* Various parameters */);

    if(status ! = Status.RUNNING) { loadStatus =null; }}Copy the code

Then, let’s focus on Engine’s load() method. This method is not long, but it contains a lot of important content. This is where Glide’s caching, which we’ll look at in the next article, is implemented. Roughly logically, this method tries to find the specified resource from the memory cache and prepares to use DecodeJob to load the image when it is not in memory.

  public <R> LoadStatus load(/* Various parameters */) { EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource<? > active = loadFromActiveResources(key, isMemoryCacheable);if(active ! =null) {
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      return null; } EngineResource<? > cached = loadFromCache(key, isMemoryCacheable);if(cached ! =null) {
      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
      return null; } EngineJob<? > current = jobs.get(key, onlyRetrieveFromCache);if(current ! =null) {
      current.addCallback(cb);
      return new LoadStatus(cb, current);
    }

    EngineJob<R> engineJob = engineJobFactory.build(
            key,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache);

    DecodeJob<R> decodeJob = decodeJobFactory.build(/* Various parameters */);

    jobs.put(key, engineJob);

    engineJob.addCallback(cb);
    engineJob.start(decodeJob);

    return new LoadStatus(cb, engineJob);
  }
Copy the code

The above method involves two classes, one is DecodeJob and the other is EngineJob. The connection is that EngineJob maintains a pool of threads that manage resource loading and notify callback when the resource is loaded. DecodeJob inherits Runnable and is a task in the thread pool. As above, we start resource loading by calling engineJob.start(decodeJob).

3.1.2 summary

According to the above analysis, it is not difficult for us to get the above flow chart. Caching aside, the logic of this part is fairly clear: After calling into(), a Request object SingleRequest is constructed, and the track() method of RequestManager is called to manage Request and Target. The Request is then started using the begin() method of Request; Engine’s load() method is used to decide whether to fetch resources from the cache or load data from the data source; To load data from a data source, build a DecodeJob and give it to the EngineJob.

3.2 Stage 2: The process of turning on the network flow

3.2.1 Process of Enabling network Traffic

In the above analysis, give the DecodeJob to the EngineJob. Because DecodeJob is a task, it will be executed in a thread pool. So if we’re going to keep tracking, we should start with DecodeJob’s run() method:

So if you want to find the logic to load resources and decode, you should look at the run() method of DecodeJob. Here is the definition of this method:

  public void run(a) { DataFetcher<? > localFetcher = currentFetcher;try {
      if (isCancelled) {
        notifyFailed();
        return;
      }
      runWrapped();
    } catch (Throwable t) {
      if(stage ! = Stage.ENCODE) { throwables.add(t); notifyFailed(); }if(! isCancelled) {throwt; }}finally {
      if(localFetcher ! =null) { localFetcher.cleanup(); } GlideTrace.endSection(); }}Copy the code

DecodeJob execution uses state mode, which determines the method to execute based on the current state. In the above method, the runWrapped() method is entered when the current task has not been cancelled. This method uses runReason as the current state to determine the logic to execute:

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

Here runReason is an enumeration type that contains enumeration values of the three types above. When a procedure completes, we call back to the methods in DecodeJob to modify runReason, and then execute the new logic based on the new state value.

In addition to runReason, the DecodeJob variable stage is also used to determine the DecodeJob state. Again, it is an enumeration that represents the data source to be loaded and the loading status of the data. It is modified primarily in the runGenerators(), runWrapped(), and getNextStage() methods when loading data. The usual logic is to fetch the data from the converted cache (size, size, etc.) first, then from the unconverted cache if not, and finally from the original data source if not available.

That’s how DecodeJob’s state mode works.

For a new task, runReason is set to INITIALIZE in the init() method of the DecodeJob, so we first go to INITIALIZE in the switch above. Then, because we haven’t set a policy for disk caching, we use the default AUTOMATIC caching. Therefore, we will pull data from each cache in sequence as described above. Since we are loading for the first time and we do not worry about caching for the time being, the final data loading will be handed over to the SourceGenerator.

I don’t know if you remember the relevant classes we talked about in the last article when we talked about using OkHttp in Glide. Where they really come into play is in this method down here. This is the startNext() method of SourceGenerator, which will:

  1. Use the firstDecodeHelpergetLoadData()Method to find the corresponding image type from the registered mapping tableModelLoader;
  2. And then use itDataFetcherloadData()Method loads data from the original data source.
  public boolean startNext(a) {
    if(dataToCache ! =null) {
      Object data = dataToCache;
      dataToCache = null;
      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;
        loadData.fetcher.loadData(helper.getPriority(), this); }}return started;
  }
Copy the code

Since our image is a resource on the network, we load data from the network using HttpUrlFetcher built into Glide by default. The loadData() method is defined as follows:

  public void loadData(Priority priority, DataCallback<? super InputStream> callback) {
    try {
      InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0.null, glideUrl.getHeaders());
      callback.onDataReady(result);
    } catch (IOException e) {
      callback.onLoadFailed(e);
    } finally{}}Copy the code

Obviously, after opening the InputStream from the network we got an InputStream and then we used a callback to return it. The loadDataWithRedirects() method uses HttpURLConnection to open the network flow, which we don’t elaborate on here.

3.2.2 summary

This completes the analysis of the second phase of the into() method, which fetches an input stream from the network. The whole process is not complicated, mainly in the DecodeJob state mode may not understand at the beginning, and some of the classes involved are not clear about their role. If you have these two doubts, then you are advised to: 1) patiently think about the transition process of state mode; 2). Turn over the previous article to understand the design purpose of several classes of custom Glide picture loading mode; 3). Most importantly, read the source code.

3.3 Stage 3: The process of converting an input stream to a Drawable

3.3.1 Process of converting Drawable

In the previous section we turned on the network stream. With Android’s own BitmapFactory, we can easily get a Drawable from the input stream. So why is the process of transformation divided into a separate phase?

In fact, the conversion process here is not much simpler than opening the input stream above. This is because it involves transcoding and converting the picture to a size appropriate for the control. Ok, let’s take a look at what happens in this process.

First, from loadData() above, we can see that the onDataReady() method is called back when we get the input stream. This method is called back from HttpUrlFetcher all the way to SourceGenerator. Here it uses the default disk caching policy to determine whether the data can be cached and to decide whether to cache the data or continue callback.

  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if(data ! =null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;
      cb.reschedule(); / / 1
    } else{ cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher, loadData.fetcher.getDataSource(), originalKey); }}Copy the code

Since our data was loaded using HttpUrlFetcher, we’ll go to 1 and continue processing. At this point, DecodeJob will execute once from the run() method based on the current state and call the startNext() method of the DataCacheGenerator again. The difference this time, however, is that the data already exists for caching. So, the following methods will be triggered:

  private void cacheData(Object dataToCache) {
    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);
    } finally {
      loadData.fetcher.cleanup();
    }

    sourceCacheGenerator =
        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
  }
Copy the code

The main logic here is to build a DataCacheGenerator for caching data onto disk. The process of DataCacheGenerator is basically the same as that of SourceGenerator, which is to find the ModelLoader based on the type of resource file and load the cached resource using the DataFetcher. Unlike before, this time DataFecher is used to load resources of type File. That is, when we get data from the network, Glide will cache it on disk, and it will read the image from disk and display it on the control. So, when the input stream is opened from the network, the SourceGenerator is basically done, and the rest of the display is done by the DataCacheGenerator.

As with HttpUrlFetcher, a resource of type File will be loaded by ByteBufferFetcher and the onDataReady() method will also be called when it is loaded. At this point, onDataReady() of the DataCacheGenerator is called:

  public void onDataReady(Object data) {
    cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.DATA_DISK_CACHE, sourceKey);
  }
Copy the code

This method will continue to call back to the onDataFetcherReady() method of the DecodeJob.

  // DecodeJob#onDataFetcherReady()
  public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher
        fetcher, DataSource dataSource, Key attemptedKey) {
    / /... Assignment, slightly
    if(Thread.currentThread() ! = currentThread) { runReason = RunReason.DECODE_DATA; callback.reschedule(this);
    } else {
      try {
        // Decode the data to get the desired resource type
        decodeFromRetrievedData();
      } finally{ GlideTrace.endSection(); }}}// DecodeJob#decodeFromRetrievedData()
  private void decodeFromRetrievedData(a) {
    Resource<R> resource = null;
    try {
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
      / /... Exception handling
    }
    / /... Release resources and error retries
  }

  // DecodeJob#decodeFromData()
  private <Data> Resource<R> decodeFromData(DataFetcher
        fetcher, Data data, DataSource dataSource) throws GlideException {
    try {
      / /... slightly
      Resource<R> result = decodeFromFetcher(data, dataSource);
      return result;
    } finally{ fetcher.cleanup(); }}// DecodeJob#decodeFromFetcher()
  private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource)
      throws GlideException { LoadPath<Data, ? , R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());return runLoadPath(data, dataSource, path);
  }

  // DecodeJob#runLoadPath()
  private <Data, ResourceType> Resource<R> runLoadPath(Data data, DataSource dataSource, LoadPath
       
         path)
       ,> throws GlideException {
    / /... Obtaining Parameter Information
    try {
      // Continue processing with LoadPath
      return path.load(
          rewinder, options, width, height, new DecodeCallback<ResourceType>(dataSource));
    } finally{ rewinder.cleanup(); }}// LoadPath#load()
  public Resource<Transcode> load(DataRewinder<Data> rewinder, @NonNull Options options, int width,
      int height, DecodePath.DecodeCallback<ResourceType> decodeCallback) throws GlideException {
    try {
      // Continue loading
      return loadWithExceptionList(rewinder, options, width, height, decodeCallback, throwables);
    } finally{ listPool.release(throwables); }}// LoadPath#loadWithExceptionList()
  private Resource<Transcode> loadWithExceptionList(/* Various parameters */) throws GlideException {
    Resource<Transcode> result = null;
    for (int i = 0, size = decodePaths.size(); i < size; i++) {
      DecodePath<Data, ResourceType, Transcode> path = decodePaths.get(i);
      try {
        // Continue processing with DecodePath
        result = path.decode(rewinder, width, height, options, decodeCallback);
      } catch (GlideException e) {
        exceptions.add(e);
      }
      if(result ! =null) {
        break; }}return result;
  }
Copy the code

After a series of aggressive actions above, we enter the loadWithExceptionList() method, which filters the DecodePath to get the type of image we want. DecodePath’s decode() method is called in this method. This approach is important because it is like a fork in the road: the code at 1 is the process of converting the data into the image we want; The code in position 2 is the process of processing and displaying the desired image after it has been obtained.

  // DecodePath#decode()
  public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, int height,
      Options options, DecodeCallback<ResourceType> callback) throws GlideException {
    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options); / / 1
    Resource<ResourceType> transformed = callback.onResourceDecoded(decoded); / / 2
    return transcoder.transcode(transformed, options);
  }
Copy the code

Then, let’s continue along the decodeResource(). It calls the following loop to match the current data type with the desired final image type to determine the ResourceDecoder to continue processing.

  private Resource<ResourceType> decodeResourceWithList(/* Various parameters */) throws GlideException {
    Resource<ResourceType> result = null;
    for (int i = 0, size = decoders.size(); i < size; i++) {
      ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
      try {
        DataType data = rewinder.rewindAndGet();
        if(decoder.handles(data, options)) { data = rewinder.rewindAndGet(); result = decoder.decode(data, width, height, options); }}catch (IOException | RuntimeException | OutOfMemoryError e) {
        exceptions.add(e);
      }

      if(result ! =null) {
        break; }}return result;
  }
Copy the code

ResourceDecoder has a number of implementation classes, such as BitmapDrawableDecoder, ByteBufferBitmapDecoder, etc. The name also indicates that it is used to convert one type to another.

In our program we will use ByteBufferBitmapDecoder to specialise ByteBuffer into Bitmap. It will eventually call BitmapFactory’s decodeStream() method in Downsampler’s decodeStream() method to get the Bitmap from the input stream. (Our ByteBuffer is first converted to an input stream in ByteBufferBitmapDecoder.)

  private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options, DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException {
    / /... slightly
    TransformationUtils.getBitmapDrawableLock().lock();
    try {
      result = BitmapFactory.decodeStream(is, null, options);
    } catch (IllegalArgumentException e) {
      / /... Error handling, omitted
    } finally {
      TransformationUtils.getBitmapDrawableLock().unlock();
    }

    if (options.inJustDecodeBounds) {
      is.reset();
    }
    return result;
  }
Copy the code

All that’s left is to keep retracting and retracting until we get to the fork in the road we talked about above. This ends the logic for loading images from the input stream 🙂

3.3.2 rainfall distribution on 10-12 summary

How about that? Is that a lot more complicated than opening the input stream? After all, this part involves fetching and writing data from and to the cache, which is considered the core part. Overall, this part of the design is very clever, using state mode to determine the next Generator based on the current state. Taking the input stream from the network and then using the DataCacheGenerator to read the data from the cache was a process that I didn’t even notice when I first read the source code, so much so that I was convinced it was designed this way after debugging the reasoning…

3.4 Stage 4: The process of displaying Drawable to ImageView

According to the above analysis, we have got the image data from the network, put it in the cache, and then take the data out of the cache for display. The above process is complicated, and the next stage is not easy either…

3.4.1 Process of finally displaying pictures

In the above analysis, we have reached the so-called fork in the road, and here we give the definition of this method as follows. The above analysis went to code 1, and now we continue the analysis from code 2.

  // DecodePath#decode()
  public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, int height,
      Options options, DecodeCallback<ResourceType> callback) throws GlideException {
    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options); / / 1
    Resource<ResourceType> transformed = callback.onResourceDecoded(decoded); / / 2
    return transcoder.transcode(transformed, options); / / 3
  }
Copy the code

The callback method is called here, which eventually calls back to the onResourceDecoded() method of the DecodeJob. The main logic is to change according to the parameters we set, that is, if we use a parameter like centerCrop, it will be processed here. Transformation is an interface with a set of implementations that correspond to parameters such as scaleType.

  <Z> Resource<Z> onResourceDecoded(DataSource dataSource, Resource<Z> decoded) {
    Class<Z> resourceSubClass = (Class<Z>) decoded.get().getClass();
    Transformation<Z> appliedTransformation = null;
    Resource<Z> transformed = decoded;
    // Transform the image resource
    if(dataSource ! = DataSource.RESOURCE_DISK_CACHE) { appliedTransformation = decodeHelper.getTransformation(resourceSubClass); transformed = appliedTransformation.transform(glideContext, decoded, width, height); }if(! decoded.equals(transformed)) { decoded.recycle(); }/ /... Caches related logic, omitted
    return result;
  }
Copy the code

After the transformation of the graph in the above method, the image will be cached according to the image cache policy. And then this method just returns our transformed image. So we’re back at the fork in the road. As the program continues, it reaches line 3 of the fork in the road method. Resouces

is also returned using the Transcode () method of BitmapDrawableTranscoder. It’s just that there’s a layer wrapped with BitmapDrawableTranscoder, which is lazy initialization.

Thus, when line 3 completes, our fork in the road method is finished. And then you just keep returning up. So, we go back to the decodeFromRetrievedData() method of the DecodeJob as follows. This will enter 1 of the following methods to complete the final image display operation.

  private void decodeFromRetrievedData(a) {
    Resource<R> resource = null;
    try {
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
      throwables.add(e);
    }
    if(resource ! =null) {
      notifyEncodeAndRelease(resource, currentDataSource); / / 1
    } else{ runGenerators(); }}Copy the code

The onResourceReady() method to the DecodeJob is as follows. We won’t post this part of the code because the logic of the process to reach the following method is relatively simple.

  public void onResourceReady(Resource<R> resource, DataSource dataSource) {
    this.resource = resource;
    this.dataSource = dataSource;
    MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
  }
Copy the code

A message is retrieved and sent to a Handler for processing. When the Handler receives the message, it calls EncodeJob’s handleResultOnMainThread() method to continue processing:

  void handleResultOnMainThread(a) {
    / /... slightly
    engineResource = engineResourceFactory.build(resource, isCacheable);
    hasResource = true;

    engineResource.acquire();
    listener.onEngineJobComplete(this, key, engineResource);

    for (int i = 0, size = cbs.size(); i < size; i++) {
      ResourceCallback cb = cbs.get(i);
      if(! isInIgnoredCallbacks(cb)) { engineResource.acquire(); cb.onResourceReady(engineResource, dataSource);/ / 1
      }
    }
    engineResource.release();

    release(false /*isRemovedFromQueue*/);
  }
Copy the code

After a series of judgments, the program enters code 1 and continues the callback. The cb in this case is SingeleRequest.

The program gets to the SingleRequest method and then calls the Target method in code 1 below. And the Target here is what we called ImageViewTarget.

  private void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {
    boolean isFirstResource = isFirstReadyResource();
    status = Status.COMPLETE;
    this.resource = resource;

    isCallingCallbacks = true;
    try {
      / /... slightly

      if(! anyListenerHandledUpdatingTarget) { Transition<?super R> animation =
            animationFactory.build(dataSource, isFirstResource);
        target.onResourceReady(result, animation); / / 1}}finally {
      isCallingCallbacks = false;
    }

    notifyLoadSuccess();
  }
Copy the code

When the program reaches the ImageViewTarget, it uses the setResource() method and finally calls the ImageView method to display the Drawable onto the control.

  protected void setResource(@Nullable Drawable resource) {
    view.setImageDrawable(resource);
  }
Copy the code

This completes the loading process for our Glide.

3.4.2 summary

This is how we display our previously obtained Drawable onto the control. This method contains some logic, involves a lot of code, but the overall logic is relatively simple, so this section is not long.

4, summarize

The above content is the whole process of our Glide loading picture. As can be seen from the length of the article and the code involved, the whole process is quite complicated. On the whole, the process of starting and finally displaying pictures before Glide is relatively simple and logical. The most complex and core part of the DecodeJob is the state switch.

In the previous article, we focused on the whole process of image loading. I did not give too much introduction to image caching and the loading process of cached images. We’ll cover this in the next article.

The above.

Glide Series:

  1. Glide series -1: common configuration and principle of preheating and Glide
  2. Glide Series 2: Main Process source Code Analysis (4.8.0)
  3. Glide Series 3: Implementation Principle of Glide Cache (4.8.0)

If you like my articles, you can follow me on the following platforms:

  • Blog: shouheng88. Making. IO /
  • The Denver nuggets: juejin. Cn/user / 368521…
  • Github:github.com/Shouheng88
  • CSDN:blog.csdn.net/github_3518…
  • Weibo: weibo.com/u/540115211…

More articles: Gihub: Android-Notes