preface

A powerful framework, its back without a variety of packaging, when we head into the look, it is easy to be source code in a variety of jump, design patterns, packaging and so on, do in the fog;

In this case, we can only understand the general process, but it is easy to forget, why?

Because we don’t really understand it yet!

We don’t convert them into knowledge, so we tend to forget them after a while.

So how do we translate that into our knowledge?

I don’t know if you have found that it is difficult for our brains to remember a very long thing at once, but a noun or a thing can be easily remembered;

Below I will source into a small part, and then by simple into deep, with this Angle to understand Glide source, let you like a tiger with wings added!

The length of the article is long, it is recommended to like the collection slowly watch!

Implementation ‘com. Making. Bumptech. Glide: glide: 4.12.0’ annotationProcessor ‘com. Making. Bumptech. Glide: the compiler: 4.12.0’

1, the first version of this implementation

One day, Xiao Ming wants to achieve a load picture function, so three five divided by two of the function to achieve;

The function is simple; Step 1: Download the image; Step 2: Set the image to imageView;

The code is also very simple, just a few lines to implement, see the pseudocode below:


public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        ImageView image = findViewById(R.id.image);
        String url = "Image Link";
        // Visit the web and download the images
        Bitmap bitmap = downLoadImage(url);
        // Set the image bitmap to ImageView
        image.setImageBitmap(bitmap);
    }

    public Bitmap downLoadImage(String url) {
        // Visit the web and download the images.returnbitmap; }}Copy the code

Does this feature seem like an instant memory? This feature is so simple that you don’t even have to think about it to remember it;

But this function is not universal, so Xiao Ming decided to pull it out, designed into a framework, later in other pages useful, you can take it directly to use;

2. Implementation of the second version

Therefore, Xiao Ming made the first step of transformation, and designed a RequestManager for the management of request operations, with open, pause, close network request operations, but also a life cycle monitoring, in the life cycle destruction, close the network request;

After the encapsulation, you have the following methods:


public class RequestManager {

    public void startRequest(a) {
        // Enable network requests
    }

    public void paustRequest(a) {
        // Pause the network request
    }

    public void onStart(a) {
        // Life cycle onStart
    }

    public void onStop(a) {
        // Life cycle onStop
    }

    public void onDestroy(a) {
        // Lifecycle onDestroy}}Copy the code

Now, of course, the question is, how is the RequestManager lifecycle callback?

There are many ways to do this. The simplest way is to manually call back in an Activity or fragment!

The second step is to design a request builder to build the request;

A complex request, must have a variety of parameters, callback listening, load failed image Settings and so on;

So the design looks something like this:


public class RequestBuilder {

    public RequestBuilder load(@Nullable Object model) {
        // Assign the loaded parameter
        return this;
    }

    public RequestBuilder listener(@Nullable RequestListener requestListener) {
        // Add the listener
        return this;
    }

    public RequestBuilder error(@DrawableRes int resourceId) {
        // Set the wrong image
        return this; }}Copy the code

If you have a RequestBuilder, you need to create a RequestBuilder to build a Request. If you have a RequestBuilder, you need to create a RequestBuilder to build a Request.

public interface Request {
    // Start the request
    void begin(a);

    // Clear the request
    void clear(a);

    // Pause the request
    void pause(a);
}
Copy the code

The Request class is already designed, so the image is fetched at the start of the Request;

After the picture request, or loading failure, loading booth diagram, need to set up the control, so also need to design a class, specially for setting picture control;

So Xiaoming designed an ImageViewTarget, used to set the image control, see the pseudocode below;

public class ImageViewTarget {

    public void onLoadStarted(@Nullable Drawable placeholder) {
        // Start loading and set the image booth
    }

    public void onLoadFailed(@Nullable Drawable errorDrawable) {
        // Failed to load, set image loading failed booth
    }

    public void onResourceReady(@NonNull Drawable resource) {
        // Load successfully, set the image}}Copy the code

Then this simple combination of a, it becomes a small “frame”, has the function of getting pictures to load;

Of course, I omitted some of the code here, you just need to know the general loading process, don’t go into the details;

(1) Build the Request Request through the RequestBuilder; (2) Manage requests through the RequestManager; (3) The Request succeeds or fails to call back ImageViewTarget and set it to the image control;

Very simple, three steps to implement a small picture “frame”;

3. Implementation of version 3

The first step we have simply implemented, through the network request, load the image, set to the control;

But getting pictures from the Internet every time, in reality, is very unreasonable;

Because every time from the network to obtain, not only will lead to a waste of network resources, and will affect the loading speed, in case of a bad network, it is easy to load for a long time to come out;

So let’s go ahead and modify this frame;

How? Very simple, introduce caching;

As we all know, Android’s caching mechanism can be divided into several types, one is web disk cache, one is disk cache, one is memory cache;

Then we can design the image loading mechanism with level 3 cache according to these three situations;

A simple caching mechanism is designed, and the next step is the code implementation;

To load the image cache mechanism, we can first design an Engine class Engine, used to load the image of the third level cache;

So the design looks something like this:

public class Engine {

    public void load(a) {
        // Load the image from the level 3 cache
        
    }

    private Bitmap loadFromMemory(a) {
        // Retrieve the image from the cache
        return null;
    }

    private Bitmap loadFromCache(a) {
        // Get the image from the disk
        return null;
    }

    private Bitmap loadFromNetWork(a) {
        // Get pictures from the Internet
        return null; }}Copy the code

3.1 Design of memory cache

The first is the memory cache, the memory cache design, is very important, because of the image recycling;

Reclaim early, so the memory cache does not play a practical role, reclaim slow, and will occupy memory, waste memory space;

So how do we design this image caching algorithm?

There is a famous algorithm, the least recently used algorithm, which is very good for memory cache reclamation;

What is the least recently used algorithm, of course the word is as its name;

That is, data that has not been used for a long time is not likely to be used in the future. Therefore, when the memory usage of data reaches a certain threshold, the least recently used data should be removed.

For the algorithm logic, see this: What is the LRU (least recently used) algorithm?

So we can use this algorithm here to design the image cache;

public class LruResourceCache<T.Y> {

    private final Map<T, Y> cache = new LinkedHashMap<>(100.0.75 f.true);

    public synchronized Y put(@NonNull T key, @Nullable Y item) {
        // Cache the image
        return null;
    }

    public synchronized Y get(@NonNull T key) {
        // Get the image
        return null;
    }

    public void clearMemory(a) {
        // Clear all caches
        trimToSize(0);
    }

    protected synchronized void trimToSize(long size) {
        // Reclaim memory to the specified size, and remove the least recently used memory to the specified size}}Copy the code

Here designed several methods, save and take the operation, as well as clear the cache, reclaim the image memory operation;

Are you wondering at this point, which key should I use to access the image?

It’s easy to use the image URL, because every image has a different URL, so you can use this as a unique key;

So this simple memory cache is designed;

Next, let’s look at how disk caching is designed;

3.2 design of disk cache

The design of disk cache, in fact, is not so complicated, nothing more than the image file is stored in the mobile phone memory card, read is read from the memory card;

Unlike memory, reading and writing from disk is slower. The advantage is that disk can store more and larger image files than memory.

Because compared with memory, memory card capacity is much higher than the size of memory;

And memory, we designed a read and a write operation; Read: reads image files from disk. Write: writes image files to disk

public class DiskLruCache {

    public synchronized <T> T get(String key){
        // Obtain the disk data
        return null;
    }

    public synchronized <T> void put(String key, T value){
        // Store image data to disk}}Copy the code

3.3. Network cache design

Network cache design is very simple, that is, direct access to obtain, obtain image files;

public class NetWorkLruCache {

    public synchronized <T> T request(String url){
        // Get the network data
        return null; }}Copy the code

So at this point, a simple caching mechanism is designed;

3.4,

Then a simple picture framework is thus implemented, compared with the previous framework, more than the caching mechanism, the use of pictures has been greatly improved;

If I told you, congratulations, you have successfully mastered the implementation of Glide source code, I think I might be slapped flat;

But I want to tell you is that the above principle, is a Glide source simplification, understand the above logic, basically Glide source basic process, you have understood;

The rest is basically a more detailed implementation;

In fact, the realization of a picture framework is basically inseparable from these steps, more detailed implementation, nothing more than based on these steps to expand, encapsulation;

Understand the basic principle, and then go into the source code, will be meaningful;

4, Glide version implementation

We have achieved a simple picture frame through extraction above, although the function is there, but always feel missing something!

What’s missing?

More details are missing, such as the encapsulation of design patterns, how to decouple and reuse them better!

There is also performance optimization. Although the above framework has introduced the caching mechanism, there are still more performance optimization points to be mined.

Below I will be based on the above simple picture frame, to talk about the Glide framework implementation details, compared with our framework, what further optimization points;

4.1, RequestManager

When we implement the RequestManager, we refer to the idea of implementing the RequestManager manually in the Activity or Fragment callback;

Glide implementation is more clever, by creating an invisible Fragment to achieve life cycle callback;

Glide when calling the Glide. With (this) method, it returns a created RequestManager. When creating the RequestManager, it creates an invisible Fragment and sets it to the RequestManager. Make it life-cycle listening;

The implementation logic for creating the Fragment is in the RequestManagerRetriever’s ragmentGet method;

Take a look at the general implementation;

private RequestManager fragmentGet(
    @NonNull Context context,
    @NonNull android.app.FragmentManager fm,
    @Nullable android.app.Fragment parentHint,
    boolean isParentVisible) {
    // Create an invisible RequestManagerFragment by passing in the FragmentManager
  RequestManagerFragment current = getRequestManagerFragment(fm, parentHint);
  // Obtain the RequestManager from the RequestManagerFragment
  RequestManager requestManager = current.getRequestManager();
  if (requestManager == null) {
    Glide glide = Glide.get(context);
    // If the RequestManager is empty, create a RequestManger using an abstract factory
    requestManager =
        factory.build(
            glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
    // Check whether the current page is visible. If yes, call back to the start method;
    if (isParentVisible) {
      requestManager.onStart();
    }
    // Set the RequestManager to the fragment
    current.setRequestManager(requestManager);
  }
  return requestManager;
}
Copy the code

The implementation here isn’t too complicated, it just does two things, create an invisible RequestManagerFragment, create a RequestManager, and set the RequestManger to the RequestManagerFragment, RequestManagerFragment life cycle callbacks are called back to the RequestManager;

This allows the RequestManager to have lifecycle listeners;

Lifecycle is not the Lifecycle component of the Jectpack, but a listener defined by itself that is used to call back the Lifecycle;

What optimization detail is worth discussing here?

This detail is in the creation of the RequestManagerFragment;

Requestmanagerfragments are not created every time they are fetched.

There are three steps of logic, see the source code below

private RequestManagerFragment getRequestManagerFragment(
    @NonNull final android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint) {
    // Obtain the fragment from the FragmentManager
  RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
  // If null, get from a collection of HashMaps;
  if (current == null) {
    current = pendingRequestManagerFragments.get(fm);
    // If the Fragment from the collection is empty, create a new Fragment and add it to the page. Then place it in the HashMap and send a message to remove the Fragment from the HashMap.
    if (current == null) {
      current = newRequestManagerFragment(); current.setParentFragmentHint(parentHint); . fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss(); . }}return current;
}
Copy the code

There are two main steps:

Step 1: Look for fragments in the FragmentManager and return if they are found.

Step 2: If the Fragment is not obtained in step 1, create a new Fragment and add it to the page.

In the RequestManagerFragment lifecycle method, lifecycle is called and RequestManger registers the listener so that lifecycle can be retrieved.

Finally, within the RequestManger lifecycle, images are loaded and stopped;

4.2, RequestBuilder

The RequestBuilder is used to create requests to fetch images.

This class uses the builder pattern to build parameters, which has the advantage that it is easy to add a variety of complex parameters;

The RequestBuilder does not have a build method, but it does have a into method.

The logic for loading the final image is implemented in the into method;

private <Y extends Target<TranscodeType>> Y into(
    @NonNull Y target,
    @NullableRequestListener<TranscodeType> targetListener, BaseRequestOptions<? > options, Executor callbackExecutor) {...// Create the image request
  Request request = buildRequest(target, targetListener, options, callbackExecutor);

  // Check whether the current image control has a set image request. If it has and has not been loaded,
  // Or the load is complete but fails, then the request is called begin again and the request is made again.
  Request previous = target.getRequest();
  if(request.isEquivalentTo(previous) && ! isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) { ...if(! Preconditions.checkNotNull(previous).isRunning()) { ... previous.begin(); }return target;
  }

// Clear the image request for the current image control
  requestManager.clear(target);
  // Set the request to the control
  target.setRequest(request);
  // Add the request to the requestManager
  requestManager.track(target, request);

  return target;
}
Copy the code

Here are two details:

The first detail:

Before starting the request, the previous request is fetched, and if the previous request has not been loaded yet, or if the loading has completed but failed, the request is retried.

Second detail:

What is the benefit of setting the request to the target that encapsulates the image control?

Most of our pages are list pages, so basically use the RecycleView list control to load data, and this list when loading pictures, fast sliding will appear loading disorder, the reason is the Item reuse problem of RecycleView;

Glide is here by such operations to avoid such problems;

When setRequest is called, the current Request is set to the View as a tag, so there is no confusion when fetching the Request for loading.

private void setTag(@Nullable Object tag) {... view.setTag(tagId, tag); }Copy the code

4.3, Engine

All requests are implemented in the begin of SingleRequest, and eventually in the onSizeReady of SingleRequest, where the Engine loads the image data.

The load method is divided into two main steps:

Step 1: Load data from memory

Step 2: Load data from disk or network

public <R> LoadStatus load(...). {...synchronized (this) {
  / / the first step
    memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

/ / the second step
    if (memoryResource == null) {
      returnwaitForExistingOrStartNewJob( glideContext, model, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, options, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache, cb, callbackExecutor, key, startTime); }}...return null;
}
Copy the code

As a quick review, I designed a class called LruResourceCache to do memory caching. It uses the LRU algorithm.

Where are the details of Glide compared to the logic we designed above for loading data from memory?

privateEngineResource<? > loadFromMemory( EngineKey key,boolean isMemoryCacheable, long startTime) {
  ...

  // Retrieve image resources from weak referencesEngineResource<? > active = loadFromActiveResources(key);if(active ! =null) {...return active;
  }

 // Retrieve the image from the LRU cacheEngineResource<? > cached = loadFromCache(key);if(cached ! =null) {...return cached;
  }

  return null;
}
Copy the code

First the first detail:

Glide designed a weak reference cache, when loading from memory, will first get the image resources from the weak reference;

Why design an extra layer of caching for weak references?

Weak reference objects are reclaimed when the Java VIRTUAL Machine triggers a GC, regardless of whether memory is available!

The advantage of designing a weak reference cache is that during the period when GC is not triggered, image resources can be reused to reduce the operation from the LruCache.

Take a look where the source code ends up calling:

synchronizedEngineResource<? > get(Key key) {// Get a weak reference object based on Key
  ResourceWeakReference activeRef = activeEngineResources.get(key);
  if (activeRef == null) {
    return null; } EngineResource<? > active = activeRef.get(); .return active;
}
Copy the code

Second detail:

After the image resource is obtained from LruCache, it is stored in the weak reference cache.

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

Another operation is that after the image is loaded, the image resource will be stored in the weak reference cache for subsequent reuse;

Its source location is called here: onEngineJobComplete for Engine;

This method is called in the image load callback, which is the onResourceReady of EngineJob

public synchronized void onEngineJobComplete( EngineJob
        engineJob, Key key, EngineResource
        resource) {...if(resource ! =null && resource.isMemoryCacheable()) {
   // Store it in the weak reference cacheactiveResources.activate(key, resource); }... }Copy the code

In fact, these two details are introduced by weak reference cache. It is because of a weak reference cache that the image loading performance can be squeezed to the extreme.

Some people say, well, what’s the use of this? Not much faster.

As the saying goes: performance is squeezed out step by step, in the place of optimization optimization a little bit, gradually accumulated, also into a river!

If none of the above is available, then the data will be loaded from disk or network.

4.4, DecodeJob

Reading from disk or network is a time-consuming task, so it must be executed in the child thread, and Glide is implemented in this way;

Here’s a rough look at the source code:

It will eventually create an asynchronous task called DecodeJob. What does the run method of this DecordJob do?

public void run(a) {... runWrapped(); . }Copy the code

The runWrapped method is the main one in the run method. This is where the final execution takes place.

In this com. Bumptech. Glide. Load. Engine. DecodeJob# getNextGenerator method, get the memory producers will the Generator, this a few content producers corresponding to different cache data;

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

4.5, DataCacheGenerator

ResourceCacheGenerator: indicates the producer of converted image resources.

DataCacheGenerator: corresponds to the native picture resource producer without conversion.

SourceGenerator: Corresponds to the content producer of network resources.

Here we only care about the data obtained from the disk, which corresponds to the producers of this ResourceCacheGenerator and this DataCacheGenerator;

The implementation of these two methods is similar, both by obtaining a File object, and then according to the File object to load the corresponding image data;

Glide’s disk cache is obtained from the DiskLruCache.

Let’s look at the DataCacheGenerator’s startNext method;

public boolean startNext(a) {
  while (modelLoaders == null| |! hasNextModelLoader()) { sourceIdIndex++; . Key originalKey =new DataCacheKey(sourceId, helper.getSignature());
    // Retrieve the File object from the DiskLruCache using the generated key
    cacheFile = helper.getDiskCache().get(originalKey);
    if(cacheFile ! =null) {
      this.sourceKey = sourceId;
      modelLoaders = helper.getModelLoaders(cacheFile);
      modelLoaderIndex = 0; }}...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

There are two main steps:

The first step is to retrieve the File object from the DiskLruCache using the generated key.

The second step is to convert the File object into a Bitmap object by LoadData.

Step 1: Get the File object

Glide when loading DiskLruCache, it will load the path information corresponding to all images into memory. When calling the get method of DiskLruCache, it is actually directly obtained from an Lru memory cache maintained in DiskLruCache.

So the first step, the get method, actually gets the File object from the LruCache cache;

This processing logic operates in the DiskLruCache’s open method, which triggers an operation to read the local file, namely the DiskLruCache’s readJournal method;

Finally, we went to the readJournalLine method;

This file mainly stores the image resource key, used to obtain the local image path file;

Finally, in the readJournalLine method of the DiskLruCache, you create an Entry object, and in the constructor of the Entry object you create the File object;

private Entry(String key) {...for (int i = 0; i < valueCount; i++) {
      // Create the image File object
      cleanFiles[i] = new File(directory, fileBuilder.toString());
      fileBuilder.append(".tmp");
      dirtyFiles[i] = newFile(directory, fileBuilder.toString()); . }}Copy the code

What if I downloaded the image after the DiskLruCache was initialized? At this time, the Lru memory of DiskLruCache must not have this data, then where does this data come from?

Believe smart you have guessed, is the image files stored locally will also be added to the DiskLruCache Lru memory;

Its implementation is in the Edit () method of DiskLruCache;

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {... Entry entry = lruEntries.get(key); .if (entry == null) {
    entry = newEntry(key); lruEntries.put(key, entry); }... Editor editor =newEditor(entry); entry.currentEditor = editor; .return editor;
}
Copy the code

Here, the generated Entry object is added to memory, and then the image file is written to the local through the Editor. I/O operation is not required here.

So there’s a way to save it, so let’s look at the get method of DiskLruCache;

public synchronized Value get(String key) throws IOException {...// Obtain the Entry object directly from memory;Entry entry = lruEntries.get(key); .return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
}
Copy the code

Step 2: File object transformation

When the File object is obtained according to the key, the next step is to convert the File into bitmap data;

This section of code design is very interesting, let’s see how specific implementation!

StartNext method of DataCacheGenerator:

public boolean startNext(a) {
  while (modelLoaders == null| |! hasNextModelLoader()) { ... modelLoaders = helper.getModelLoaders(cacheFile); . }while(! started && hasNextModelLoader()) {// Iterate to get the ModelLoaderModelLoader<File, ? > modelLoader = modelLoaders.get(modelLoaderIndex++);/ / get the LoadData
  loadData =
      modelLoader.buildLoadData(
          cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
  if(loadData ! =null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
   // Load the data
    started = true;
    loadData.fetcher.loadData(helper.getPriority(), this); }}return started;
}
Copy the code

4.6, ModelLoader

The ModelLoader class is designed to load the data logic.

This section of code is implemented based on the interface, no specific implementation, the benefit is very decoupled and flexible, the downside is that the code is less readable, because you are very difficult to find the implementation class here!

What exactly does this ModelLoader class do? We can look at the comments first;

A factory interface that transforms an arbitrarily complex data model into a specific data type.

How to interpret this sentence? It can be understood as the adapter pattern, which converts one type of data into another type of data.

public interface ModelLoader<Model.Data> {...class LoadData<Data> {
    public final Key sourceKey;
    public final List<Key> alternateKeys;
    public final DataFetcher<Data> fetcher;

    public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {
      this(sourceKey, Collections.<Key>emptyList(), fetcher);
    }

    public LoadData(
        @NonNull Key sourceKey,
        @NonNull List<Key> alternateKeys,
        @NonNull DataFetcher<Data> fetcher) {
      this.sourceKey = Preconditions.checkNotNull(sourceKey);
      this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
      this.fetcher = Preconditions.checkNotNull(fetcher); }}...@Nullable
  LoadData<Data> buildLoadData(
      @NonNull Model model, int width, int height, @NonNull Options options); .boolean handles(@NonNull Model model);
Copy the code

This class has very few methods, it has an internal class LoadData, and within LoadData it has an interface DataFetcher, and the DataFetcher’s job is to LoadData, and it has an interface method LoadData;

This is a typical combination pattern that allows the class to have more functionality without changing the class structure;

ModelLoader also has an interface method, buildLoadData, for building a LoadData object;

See here, do you feel understand, but not completely understand!

Yes, although we know the implementation logic here, we don’t know where the implementation is!

Here we use two interfaces, one is ModelLoader, one is DataFetcher, to find the implementation of ModelLoader, but also to find the implementation of DataFetcher, it looks very around!

Again, it all starts with ModelLoader;

When the ModelLoader is implemented, the method of building LoadData is also implemented. When the LoadData is constructed, the DataFetcher object is also constructed. In fact, the LoadData object is only responsible for maintaining DataFetcher and other related variables. It’s still the DataFetcher where the data is loaded;

In other words, if we find the ModelLoader, we find the corresponding DataFetcher, and we know the corresponding loading logic.

What is this called in terms of design? High cohesion! The same class of related implementations are put together to achieve a highly cohesive package;

In just a few lines of code, Daniel explains what it means to be highly cohesive and low coupled.

Let’s take a look at how ModelLoader gets it;

From the source code above, we can see that the ModelLoader is obtained by the DecodeHelper’s getModelLoaders method, by passing in the File object;

The final call is to get the list of ModelLoaders through Registry’s getModelLoaders method;

It’s important to talk about the Registry class here;

4.7, Registry

The main responsibility of this class is to store a variety of data for external use. You can think of this class as a set of maps, which hold a variety of data.

This Registry is just more powerful encapsulation, which can save richer content;

Take a look at the constructor of this class, which creates a bunch of registration classes to hold the corresponding data;

This class is initialized in the Glide constructor, and the corresponding data is registered in the Glide constructor;

At a glance, you don’t need to get too deep into it, except that this class is used to hold registered data for external use.

When we get the List of ModelLoader through Registry, we will iterate to determine whether the current LoadData has a LoadPath, which we will talk about next;

The only thing you need to know is that this class is used for resource conversion, such as turning resource files into bitmaps, drawables, etc.

When there is a match, it is executed only once;

And the class that we end up executing here is ByteBufferFileLoader;

This class converts the File object to a ByteBuffer when the loadData method is executed;

Take a look at the general implementation, without going too far into it;

Now that we have the stream data of the file, how do we turn it into a bitmap that can be displayed?

This is where the decoding comes in;

The ByteBuffer is finally called back to the DecodeJob class, where the decoded logic is implemented.

Take a look at the onDataFetcherReady method for DecodeJob:

public void onDataFetcherReady( Key sourceKey, Object data, DataFetcher
        fetcher, DataSource dataSource, Key attemptedKey) {... decodeFromRetrievedData(); . }Copy the code

The decodeFromRetrievedData method is called to decode the image stream data.

The LoadPath class implements the decoding function,

4.8, LoadPath

The LoadPath class is not a complicated implementation. It contains abstract variables such as Pool and DecodePath. The LoadPath class also uses a combination pattern.

But through the combination of classes to call, the implementation abstraction, so that the class can be more flexible use;

The place to decode is in the load method of the LoadPath of this class, and eventually through the decode method of DecodePath, which is not implemented in this class;

The design of the DecodePath and LoadPath is actually similar, are not involved in the concrete implementation, but through the interface held inside to call, and then the concrete implementation abstraction to the external, from the external to control;

Take a look at the member variables and constructors of this class:

In the DecodePath of the decode method, the final call is through the ResourceDecoder decode method to decode;

Take a look at the rough implementation;

The ResourceDecoder is an interface that implements many classes. See the screenshot below for details:

The final implementation is in these classes;

Take a look at the general flow chart:

Where are these classes created?

As we can see from the constructor above, this class is passed in step by step from outside;

LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath: LoadPath

First, in the DecodeJob’s decodeFromFetcher method, through the DecodeHelper’s getLoadPath method;

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

In the DecodeHelper’s getLoadPath, you can see that the LoadPath is finally obtained through the Registry class;

<Data> LoadPath<Data, ? , Transcode> getLoadPath(Class<Data> dataClass) {return glideContext.getRegistry().getLoadPath(dataClass, resourceClass, transcodeClass);
}
Copy the code

Above we talked about the Registry class, which is used to hold data for external calls. The place to add data is in Glide constructor;

Now take a look at Registry’s getLoadPath method;

public <Data, TResource, Transcode> LoadPath<Data, TResource, Transcode> getLoadPath(
    @NonNull Class<Data> dataClass,
    @NonNull Class<TResource> resourceClass,
    @NonNull Class<Transcode> transcodeClass) {
  // Get LoadPath from cache
  LoadPath<Data, TResource, Transcode> result =
      loadPathCache.get(dataClass, resourceClass, transcodeClass);
  if (loadPathCache.isEmptyLoadPath(result)) {
    return null;
  } else if (result == null) {
   // Get the DecodePath set
    List<DecodePath<Data, TResource, Transcode>> decodePaths =
        getDecodePaths(dataClass, resourceClass, transcodeClass);
    // It's possible there is no way to decode or transcode to the desired types from a given
    // data class.
    if (decodePaths.isEmpty()) {
      result = null;
    } else {
     // Create LoadPath with DecodePath and save it to cache for next use
      result =
          new LoadPath<>(
              dataClass, resourceClass, transcodeClass, decodePaths, throwableListPool);
    }
    loadPathCache.put(dataClass, resourceClass, transcodeClass, result);
  }
  return result;
}
Copy the code

This method does three main things:

(1) Get LoadPath from the cache, it can be seen that optimization is everywhere, can reuse will not be created multiple times;

(2) Obtain DecodePath set;

(3) Create LoadPath according to DecodePath and save it in cache;

There is nothing complicated about the implementation here. Let’s take a look at the DecodePath acquisition, specifically in the getDecodePaths method of Registry;

private <Data, TResource, Transcode> List<DecodePath<Data, TResource, Transcode>> getDecodePaths(
    @NonNull Class<Data> dataClass,
    @NonNull Class<TResource> resourceClass,
    @NonNull Class<Transcode> transcodeClass) {
  ...

  for (Class<TResource> registeredResourceClass : registeredResourceClasses) {
    ...

    for (Class<Transcode> registeredTranscodeClass : registeredTranscodeClasses) {

      // Create a DecodePath from the ResourceDecoder data registered with the Glide constructor
      List<ResourceDecoder<Data, TResource>> decoders =
          decoderRegistry.getDecoders(dataClass, registeredResourceClass);
      ResourceTranscoder<TResource, Transcode> transcoder =
          transcoderRegistry.get(registeredResourceClass, registeredTranscodeClass);
      @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
      DecodePath<Data, TResource, Transcode> path =
          newDecodePath<>( dataClass, registeredResourceClass, registeredTranscodeClass, decoders, transcoder, throwableListPool); decodePaths.add(path); }}return decodePaths;
}
Copy the code

LoadPath and DecodePath are created at fetch time. The ResourceDecoder data is saved by the Registry class. The DecodePath is created first, and then the LoadPath is created.

Let’s look at how the creation of the ResourceDecoder class is implemented;

Here it becomes clear that the ResourceDecoder is created by the Registry class in Glide’s constructor and stored in the collection;

Then, when decoding, the LoadPath is obtained, and then the DecodePath is created by the ResourceDecoder data saved by the Registry class.

Take a look at the general flow chart:

After looking at the logic of fetching images from the disk cache above, let’s take a look at the logic of fetching images from the network;

4.9, the SourceGenerator

The place to load the network cache is in the startNext method of the SourceGenerator class. Let’s take a look at the general implementation;

public boolean startNext(a) {
  if(dataToCache ! =null) {
    Object data = dataToCache;
    dataToCache = null;
    // Cache the datacacheData(data); }... 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; startNextLoad(loadData); }}return started;
}
Copy the code

This method does two steps, the first step is to cache the data, the second step is to download the image resources from the network;

If the following implementation looks familiar, the data is loaded through the ModelLoader as well as the disk cache logic.

The ModelLoader is also obtained by Registry, and the place of creation is also created by Registry in Glide constructor method, and cached in the cache;

The final ModelLoader called here is HttpGlideUrlLoader, and the place to load network data is the loadData method in HttpUrlFetcher;

Let’s look at the implementation of this method;

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

And the last place we call is loadDataWithRedirects;

The implementation here, in fact, is the Android native network request, by creating an HttpURLConnection to get the image data from the network;

When it succeeds, it will be saved to disk;

Because it is a native network request, there is actually no network optimization done here, so we can use a custom ModelLoader, Glide network request to switch to the OKHttp framework, I will not go into the details of OKHttp, the most powerful network request framework!

After obtaining, the decoding logic is also the same as the disk cache, also through LoadPath to decode, and convert it into Bitmap data;

The article omitted part of the source code, suggested to follow the source code to go through the logic, can be very good to deepen the impression;

There are some details that I won’t go into here because of the length of the article;

So here Glide source code is about the same, if you have other ideas are welcome to discuss with me in the comments section;

About me

Brother Dei, if my post is helpful to you, please give me a like ️, and follow me on Github and my blog.