This article is based on Glide4.11.0 source code analysis, mainly from the macro analysis Glide load a picture of the complete process, the style of this article to minimize the paste of a large number of source code, for the more important source code can be used pseudo-code display as far as possible use pseudo-code display, otherwise the analysis of code fragments for detailed annotation, Less important parts or I think everyone can be familiar with the source code will be replaced by text description or pictures.

This article first gives a Glide loading picture of the overall flow chart, and then the source of each part targeted to analyze, as well as some parts of the analysis after a small summary, and finally introduced the decoding logic and transformation process in detail.

1. Basic execution process

Glide.with(context).load("xxx").into(imageView);
Copy the code

Executing the code above performs the following process

Click for a larger version

2. Each module is introduced

2.1 Life cycle module

2.1.1 Life cycle module UML diagram

Click to see the original image

2.1.2 Introduction to each part

  1. LifecycleListener

    LifecycleListener interface classes that implement onStart(),onStop(), and onDestroy() in LifecycleListener interface classes when the activity lifecycle to which Gldie is attached changes. According to UML, the RequestManager implements LifecycleListener, so the RequestManager can control the suspension, resumption, and cancellation of requests based on lifecycle changes.

  2. Lifecycle

    The management Lifecycle interface class manages classes that implement the LifecycleListener interface, which adds and removes lifecycle observations.

  3. ActivityFragmentLifecycle

    Lifecycle implementation class, used to distribute the Lifecycle uniformly, add an instance of implementing LifecycleListener to the collection by calling addListener(LifecycleListener Listener). Remove it from the collection by removeListener(LifecycleListener Listener) and unify the distribution lifecycle by iterating through the collection with internally defined onStart(), onStop(), and onDestory().

  4. SupportRequestManagerFragment

    Life cycle emission source, namely when the Glide FragmentActivity or androidx. Fragments. The app. The fragments (if it is before androidx is supportv4 fragments) under the package subclasses loads, Will be added to the current activities of a transparent Fragment, the Fragment is SupportRequestManagerFragment; The purpose is to sense life cycle changes and to suspend, resume, and cancel requests.

  5. Request

    The top-level interface of the request class, which defines the start, pause, cancel, and other operations of the request.

  6. RequestTracker

    Classes for tracing, canceling, and restarting requests, as well as ongoing, completed, and failed requests.

  7. Target

    All the different loading methods are finally loaded via Target#onResourceReady(@nonnull R resource, @nullable Transition
    transition), and different targets have different ways of displaying resources. If you load a bitmap and if you load a drawable, it’s presented differently with setImageBitmap(Resource) and setImageDrawable(resource), same thing with a failed load callback; The Target design is an interface design designed to display different resources to different targets or the same Target.

    The Target lifecycle events are as follows:

    • OnLoadStarted is called when the load starts
    • OnResourceReady Is called when the resource is successfully loaded
    • OnLoadCleared is called when the load is cancelled and its resource is released
    • OnLoadFailed Called when resource loading fails

A typical lifecycle is onLoadStarted ->onResourceReady or onLoadFailed ->onLoadCleared; However, there are no guarantees. OnLoadStarted cannot be called if the resource is in memory if loading fails because the model object is empty. Similarly, onLoadCleared may never be called.

  1. RequestManager

    LifeCycle, RequestTracker, TargetTracker, load(XXX), asBitmap(),asDrawable(), etc. RequestManager is defined to manage Target and Request, and to notify Target and Request uniformly when lifecycle changes are made. More importantly, RequestManager is a call layer access to a set of apis called GlideBuilder.

2.1.3 Life cycle Module summary

Because the module code is relatively simple throughout its lifecycle, I will not analyze the code here, but only explain the role of the various classes associated with the module. The whole life cycle module is actually a transparent Fragment created by Glide. With (Context) to observe the life cycle changes of the activity. Through ActivityFragmentLifecycle will be distributed to RequestManager life cycle changes, whereas the RequestManager could in different lifecycle callback methods intelligence to stop, and to request for the request.

2.2 Requesting module initialization

2.2.1 Request initialization module code analysis

The request initialization starts with a call to the into function, and the first parameter target, which is the DrawableImageViewTarget, TargetListener is null if not set,options is the instance object that calls the current into method, RequestBuilder(RequestBuilder) ¶ RequestBuilder(RequestBuilder) is a RequestBuilder subclass of BaseRequestOptions.

private <Y extends Target<TranscodeType>> Y into(
    @NonNull Y target,
    @NullableRequestListener<TranscodeType> targetListener, BaseRequestOptions<? > options, Executor callbackExecutor) {
  // The request is SingleRequest
  Request request = buildRequest(target, targetListener, options, callbackExecutor);
  Request previous = target.getRequest();
  // Whether the previous request can be reused
  // Reuse conditions: 1: same as the previous request; 2: The previous request was not completed or the operation of the current request allows memory caching
  if(request.isEquivalentTo(previous) && ! isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {if(! Preconditions.checkNotNull(previous).isRunning()) {// Restart the load if the previous request has completed
      // Use the previous request rather than the new one to allow for optimizations like skipping
      // setting placeholders, tracking and un-tracking Targets, and obtaining View dimensions
      // that are done in the individual Request.
      previous.begin();
    }
    return target;
  }
  // This is a new request, so you need to clear the old request bound by target
  requestManager.clear(target);
  //target binds a new request
  target.setRequest(request);
  // Add target to targetTracker within requestManager; Add the request to the requestTracker within the requestManager
  // And start executing the request
  requestManager.track(target, request);
  return target;
}
Copy the code

From the above code analysis, it can be seen that before executing the request, it determines whether the old request can be reused. If it can be reused, it determines whether the request needs to be re-executed based on the state. If it cannot be reused, the old request binding is cleared from target, the new request is bound, and the request is executed in requestManager#track(target, request). This verifies that we described RequestManager responsibilities earlier in the lifecycle module as RequestManager, which is defined to manage Target and Request.

Requestmanager.track (target, request)

synchronized void track(@NonNullTarget<? > target,@NonNull Request request) {
  // Add target to targetTracker
  targetTracker.track(target);
  // Add the request to the requestTracker and execute it
  requestTracker.runRequest(request);
}

public void runRequest(@NonNull Request request) {
    requests.add(request);
    if(! isPaused) {// Execute the request
        request.begin();
    } else {
        request.clear();
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Paused, delaying request"); } pendingRequests.add(request); }}Copy the code

Request instance SingleRequest (). Request instance SingleRequest. Request#begin().

@Override
public void begin(a) {
  synchronized (requestLock) {
    if (model == null) {...// The url is empty. Loading failed
      onLoadFailed(new GlideException("Received null model"), logLevel);
      return;
    }
    if (status == Status.COMPLETE) {
       // The load is complete, avoid reloading
       onResourceReady(resource, DataSource.MEMORY_CACHE);
       return;
    }  
    status = Status.WAITING_FOR_SIZE;
    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
      // If the width and height are specified, the load starts
      onSizeReady(overrideWidth, overrideHeight);
    } else {
      . / / if not registered ViewTreeObserver OnPreDrawListener listening, such as access to the actual size is again call onSizeReady began to load
      target.getSize(this);
    }
    if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
        && canNotifyStatusChanged()) {
      / / set the placeholdertarget.onLoadStarted(getPlaceholderDrawable()); }}}@Override
public void onSizeReady(int width, int height) {
    synchronized (requestLock) {
        ...
            // Engine #load is created by GlideEngine.load (XXX omits a lot of arguments); . }}Copy the code

To recap, Engine is responsible for starting loads and managing active and memory caches

The execution process is as follows:

  1. Checks the active cache currently in use and returns the active resource if it exists
  2. Check the memory cache, if present, and provide the cache resource, and move the memory cache resource to the active cache
  3. Check the current load in progress and add the load result callback to the load in progress
  4. Start a new load.

Ps: Cache related source code will be analyzed in the subsequent cache module

The engine#load() pseudocode is shown below

public <R> LoadStatus load(...). { EngineKey key =... EngineResource<? > memoryResource;synchronized (this) {
    memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
    if (memoryResource == null) {
      // If there is none in the active cache or memory cache, a new load or reuse of available request tasks is started
      return waitForExistingOrStartNewJob(...);
    }
  }
  // If the active cache or memory cache exists, the callback succeeds
  cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
  return null;
}
Copy the code

If the first three are already loaded resources or available requests, let’s go straight to step 4 and start the new load.

So here since waitForExistingOrStartNewJob analysis directly

private <R> LoadStatus waitForExistingOrStartNewJob(ResourceCallback cb,
                                                    booleanonlyRetrieveFromCache, Executor callbackExecutor,... // omit a lot of arguments) { EngineJob<? > current = jobs.get(key, onlyRetrieveFromCache);if(current ! =null) {
     // There are reusable request tasks
    current.addCallback(cb, callbackExecutor);
    return new LoadStatus(cb, current);
  }   
  // Create EngineJob
      
  EngineJob<R> engineJob =EngineJob<R>();
  // Pseudo-code creates DecodeJob and adds decoder callback
  DecodeJob<R> decodeJob =new DecodeJob<R>(DecodeJob.Callback<R> callback)
  // Cache upcoming tasks for reuse purposes
  jobs.put(key, engineJob);
  // Add callback ResourceCallback and callbackExecutor to engineJob
  engineJob.addCallback(cb, callbackExecutor);
  // Start executing
  engineJob.start(decodeJob);
  return new LoadStatus(cb, engineJob);
}
Copy the code

It can be seen from the above code analysis that the EngineJob is a bridge between the callback to the Target and the DecodeJob. It is also responsible for executing the DecodeJob, which is the specific task and decoding after the task is executed. Set when creating the DecodeJob DecodeJob. Callback listener, realized EngineJob DecodeJob. Callback, namely request Callback to decode the finished results EngineJob, After creating the EngineJob, set ResourceCallback listener and main thread executor for the EngineJob, that is, call back to the EngineJob from the main thread executor and call back to ResourceCallback. So it goes from EngineJob callback to SingleRequest, from SingleRequest callback to Target, and Target is DrawableImageViewTarget, The ImageView is setImageDrawable(resource) in the DrawableImageViewTarget callback result.

2.2.3 Request Initialization Summary

To sum up what the Request initialization module does, it calls into to create Target and Request. After creating Target and Request, it binds Target and Request bidirectionally. Then it starts to execute the Request. Otherwise, listen on the ImageView and wait until it gets its true size before loading. The loading process is as follows:

  1. Checks the active cache currently in use and returns the active resource if it exists
  2. Check the memory cache, if present, and provide the cache resource, and move the memory cache resource to the active cache
  3. Check the current load in progress and add the load result callback to the load in progress
  4. Start a new load.

Steps 1,2 and 3 belong to the resource acquisition of the memory cache and listen for the requests being loaded. DecodeJob DecodeJob DecodeJob DecodeJob DecodeJob DecodeJob Again from EngineJob – > SingleRequest – > DrawableImageViewTarget – > setImageDrawable (resource).

2.3 Data request module

The data request module is divided into request from disk cache and request from network. If no, obtain it from the network. No matter which request way, the final output is the same, so for different request way of different or the same parameter input, how to convert to the same output, this is Glide needs to data input and output model for more abstract design, that is, componentized design.

2.3.1 Glide component design

When Glide is created, a Registry object is created and the specific input -> output model is registered with the appropriate Registry module. According to functional modules, Registry is divided into:

  • ModelLoaderRegistry: Data loading module
  • EncoderRegistry: Encoding storage module that provides the ability to persist data to disk files
  • ResourceDecoderRegistry: A decoding module that can decode various types of data, such as files and byte arrays, into resources such as bitmaps and drawable
  • ResourceEncoderRegistry: Encoding storage module that can persist resource files such as bitmap and drawable
  • DataRewinderRegistry: data stream redirection module, such as position in ByteBuffer or pointer position in stream, etc
  • TranscoderRegistry: Transcoding module that provides the ability to transcode different resource types, such as converting a bitmap to a drawable
  • ImageHeaderParserRegistry image parsing module
 Glide(...) {
    registry = new Registry();
    / /... Omit numerous registers and appends
	registry.append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())
    ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory();
    glideContext = newGlideContext(registry,...) ; }Copy the code

So the modules here that are relevant to our data request module are ModelLoaderRegistry, EncoderRegistry, ResourceDecoderRegistry, TranscoderRegistry, That is to take the data and encode it and cache it on disk and then decode the encoded product, transcode it and then display it.

The whole Glide componentization design idea is the same, here only ModelLoaderRegistry and EncoderRegistry are analyzed.

  • ModelLoaderRegistry analysis

When registry. Append is called, it is proxyed to ModelLoaderRegistry, which in turn is proxyed to MultiModelLoaderFactory, So the final append data is stored in the MultiModelLoaderFactory

//MultiModelLoaderFactory#append
synchronized <Model, Data> void append(
    @NonNull Class<Model> modelClass,
    @NonNull Class<Data> dataClass,
    @NonNull ModelLoaderFactory<? extends Model, ? extends Data> factory) {
  add(modelClass, dataClass, factory, /*append=*/ true);
}
  private <Model, Data> void add(
      @NonNull Class<Model> modelClass,
      @NonNull Class<Data> dataClass,
      @NonNull ModelLoaderFactory<? extends Model, ? extends Data> factory,
      boolean append) {
    Entry<Model, Data> entry = new Entry<>(modelClass, dataClass, factory);
    entries.add(append ? entries.size() : 0, entry);
  }
Copy the code

By encapsulating data into Entry and saving it to the entries collection, data retrieval is ultimately also retrieved from entries.

What data is encapsulated in the Entry? What is a modelClass and what is a dataClass? And the factory? What good are they? Why keep them?

This brings us to Glide’s concept of abstraction as data is loaded,

When Gldie is created, multiple sets of data are added to ModelLoaderRegistry, such as from the network, from the local path, and from the assets input stream. Different inputs have different results and different processing methods, so Glide needs to be told what type is entered. What is the type of output, and what is the processing method, when Glide is loaded, it will match the input and output type, to choose different processing methods, such as here to obtain resources from the network example:

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

GlideUrl is a wrapper around a network URL, so the input is GlideUrl, the output is InputStream, and the processing is HttpGlideUrlLoader.

So with the concept of componentization clear, let’s take a closer look at the class diagram relationship of data loading namely ModelLoader and its associated class relationship.

Click to see the original image

This is just an example of HttpGlideUrlLoader. Different modelLoaders are created by ModelLoaderFactory#build. The LoadData object is created with ModelLoader#buildLoadData, and the LoadData is loaded with DataFetcher in LoadData, and the result is called back to the calling layer through DataCallback.

Therefore, the analysis of ModelLoaderRegistry data loading module is completed. Through the polymorphic mechanism, abstract model is defined and specific types are stored. At runtime, specific processing model is searched according to input and output types to complete the process from request to response.

  • EncoderRegistry analysis
registry
    .append(ByteBuffer.class, new ByteBufferEncoder())
    .append(InputStream.class, new StreamEncoder(arrayPool))
Copy the code

Two encoding components are registered during Gldie creation to encode inputs of type ByteBuffer and Type InputStream to disk.

The UML diagram is as follows:

Click to see the original image

Finally, the append data is saved to the EncoderRegistry#encoders collection, which also walks through the encoders to select the corresponding encoder based on the input type, and then writes the input data to the file.

2.3.2 Data request module source code analysis

DecodeJob implements Runnable, and DecodeJob encapsulates the thread pool execution method, so when EngineJob. Start (DecodeJob) is called, To submit a task to the thread pool, run() is called in DecodeJob.

The entire run method execution logic can be fully represented with the following pseudocode:

public void run(a) {
  try {
    if (isCancelled) {
        GlideException e = new GlideException("Failed to load resource".new ArrayList<>(throwables));
      callback.onLoadFailed(e);
      return;
    }
    // Obtain it from ResourceCacheGenerator
    Object data  = findResourceCache();
    if(data==null) {// Get it from DataCacheGenerator
       data = findDataCache();
       if(data==null) {// Get it from SourceGenerator
         Object sourceData = requestData(); 
         // If it can be cached, the data is cached to disk
         if(canCache){ 
            DataCacheGenerator.saveCache(sourceData);
            data = findDataCache(); 
         }else{ data = sourceData; }}}// Decode the obtained data
    decodeFromRetrievedData(data);      
  } catch (Throwable t) {
    callback.onLoadFailed(e);
    throwt; }}Copy the code

Wherever the resource is obtained, the corresponding ModelLoader is matched from the ModelLoaderRegistry module based on the input and output types, and a LoadData object containing the specific loading method is created with ModelLoader#buildLoadData. The data is then loaded through the DataFetcher in LoadData and the result is called back to the calling layer through the DataCallback.

The entire data request process is described in words as follows:

Under the default caching strategies, namely diskCacheStrategy (diskCacheStrategy. AUTOMATIC)

  1. Retrieved from the disk cache after decoding transformation, and returned for decoding if present
  2. Retrieved from the source file disk cache and returned for decoding if present
  3. Get from source, get to cache data to source file cache, and return to decode

2.4 Decoding and transcoding module

In the analysis before the first to understand Glide decoding solution is what? What does transcoding transfer?

Some decoding and transcoding components are registered with the ResourceDecoderRegistry and TranscoderRegistry when Glide is created

Q1: What does decoding solve? What happens when you decode it?

Concrete decoder describe
ByteBufferGifDecoder Decode ByteBuffer to GifDrawable
ByteBufferBitmapDecoder Decode ByteBuffer to Bitmap
ResourceDrawableDecoder Decode the resource Uri to Drawable
ResourceBitmapDecoder Decode the resource ID to a Bitmap
BitmapDrawableDecoder Decode the data type to BitmapDrawable
StreamBitmapDecoder Decode InputStreams to Bitmap
StreamGifDecoder Convert the InputStream data to BtyeBufferr and decode it to GifDrawable
GifFrameResourceDecoder Decoding the GIF frame
FileDecoder Wrap the File to become a FileResource
UnitDrawableDecoder Wrap Drawable as DrawableResource
UnitBitmapDecoder Wrap a Bitmap into a BitmapResource
VideoDecoder Decode the local video file into a Bitmap
InputStreamBitmapImageDecoderResourceDecoder Decode InputStream to Bitmap(API 28)

Q2: What does transcoding translate? What happens after transcoding?

Specific transcoder describe
BitmapDrawableTranscoder Transcoding a Bitmap to BitmapDrawable
BitmapBytesTranscoder Convert Bitmap to Byte Arrays
DrawableBytesTranscoder Convert BitmapDrawable to Byte Arrays
GifDrawableBytesTranscoder Convert GifDrawable to Byte Arrays
UnitTranscoder Without conversion, what is input, what is output

DecodeJob#onDataFetcherReady = DecodeJob#onDataFetcherReady = DecodeJob#onDataFetcherReady

public void onDataFetcherReady( Key sourceKey, Object data, DataFetcher
        fetcher, DataSource dataSource, Key attemptedKey) {
  this.currentSourceKey = sourceKey;
  this.currentData = data;
  this.currentFetcher = fetcher;
  this.currentDataSource = dataSource;
  this.currentAttemptingKey = attemptedKey; . decodeFromRetrievedData(); }private void decodeFromRetrievedData(a) {
    //decodeFromData finally calls decodeFromFetcherResource<R> resource = decodeFromData(currentFetcher, currentData, currentDataSource); . notifyEncodeAndRelease(resource, currentDataSource); }private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource)
    throws GlideException {
    / / get LoadPathLoadPath<Data, ? , R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());// Execute LoadPath#load() to complete decoding and transcoding
    return runLoadPath(data, dataSource, path);
}

Copy the code

LoadPath#load(…) To complete the decoding.

Let’s take a look at getting LoadPath

public <Data, TResource, Transcode> LoadPath<Data, TResource, Transcode> getLoadPath( Class dataClass, Class
        
          resourceClass, Class
         
           transcodeClass)
         
         {.../ / get decodePaths
 	List<DecodePath<Data, TResource, Transcode>> decodePaths =
        getDecodePaths(dataClass, resourceClass, transcodeClass);
     // Wrap decodePaths in LoadPath and return
     return new LoadPath<>(dataClass, resourceClass, transcodeClass, decodePaths, throwableListPool)
    
}
 private <Data, TResource, Transcode> List<DecodePath<Data, TResource, Transcode>> getDecodePaths(
      @NonNull Class<Data> dataClass,
      @NonNull Class<TResource> resourceClass,
      @NonNull Class<Transcode> transcodeClass) {
    List<DecodePath<Data, TResource, Transcode>> decodePaths = new ArrayList<>();
    // Filter the result set of the decoded type according to the data type and decoded type
    List<Class<TResource>> registeredResourceClasses =
        decoderRegistry.getResourceClasses(dataClass, resourceClass);
    // Iterate over the decoder type result set
    for (Class<TResource> registeredResourceClass : registeredResourceClasses) {
      // Filter the conversion type result set based on the decoding type and the final conversion type
      List<Class<Transcode>> registeredTranscodeClasses =
          transcoderRegistry.getTranscodeClasses(registeredResourceClass, transcodeClass);
      // Iterate over the conversion type result set
      for (Class<Transcode> registeredTranscodeClass : registeredTranscodeClasses) {
        // Filter the decoder result set according to the data type and decoder type
        List<ResourceDecoder<Data, TResource>> decoders =
            decoderRegistry.getDecoders(dataClass, registeredResourceClass);
        // Filter transcoder according to decoding type and transcoding type
        ResourceTranscoder<TResource, Transcode> transcoder =
            transcoderRegistry.get(registeredResourceClass, registeredTranscodeClass);
        DecodePath encapsulates the data type, transcoding type, decoder type, and a set of decoders that match the data type -> specified decoder type, decoder type -> transcoding type
        DecodePath<Data, TResource, Transcode> path =
            newDecodePath<>( dataClass, registeredResourceClass, registeredTranscodeClass, decoders, transcoder, throwableListPool); decodePaths.add(path); }}return decodePaths;
  }
Copy the code

The above two layers of for loop are only for screening qualified and paired decoders and transcoders in Glide decoding module and transcoding module, and the filtered products are saved in LoadPath#decodePaths

Glide. With (context).load(” XXX “).into(imageView); This will filter out three DecodePath images and save each DecodePath into decodePaths. Each DecodePath will have the following contents:

From data type to decode type to transcoding type is as follows:

The data type | decoder decoding type Transcoding type | transcoder
ByteBuffer GifDrawable|ByteBufferGifDecoder Drawable|UnitTranscoder
ByteBuffer Bitmap|ByteBufferBitmapDecoder Drawable|BitmapDrawableTranscoder
ByteBuffer BitmapDrawable|BitmapDrawableDecoder Drawable|UnitTranscoder

Since filtered eligible data types, decoding type | decoder, transcoding type | transcoder, so the next step is applied, namely data decoding first, then the product of the decoding transcoding, so here is called LoadPath# runLoadPath to apply filter results.

LoadPath#runLoadPath

private <Data, ResourceType> Resource<R> runLoadPath( Data data, DataSource dataSource, LoadPath
       
         path)
       ,>
    throws GlideException {
  Options options = getOptionsWithHardwareConfig(dataSource);
  // Data stream redirects, such as position in ByteBuffer or pointer position in stream, etc
  DataRewinder<Data> rewinder = glideContext.getRegistry().getRewinder(data);
  try {
    // finally call LoadPath#loadWithExceptionList
    return path.load(
        rewinder, options, width, height, new DecodeCallback<ResourceType>(dataSource));
  } finally{ rewinder.cleanup(); }}//LoadPath#loadWithExceptionList
private Resource<Transcode> loadWithExceptionList(
    DataRewinder<Data> rewinder,
    @NonNull Options options,
    int width,
    int height,
    DecodePath.DecodeCallback<ResourceType> decodeCallback,
    List<Throwable> exceptions)
    throws GlideException {
    Resource<Transcode> result = null;
    // Loop over the DecodePath stored in decodePaths and call DecodePath#decode; Glide. With (context).load(" XXX ").into(imageView); So I'm going to use this group right here
    / / data type - > | decoder decoding type - > transcoding type | transcoder
    //ByteBuffer -> Bitmap|ByteBufferBitmapDecoder -> Drawable|BitmapDrawableTranscoder
    for (int i = 0, size = decodePaths.size(); i < size; i++) {
        DecodePath<Data, ResourceType, Transcode> path = decodePaths.get(i);
        try {
            result = path.decode(rewinder, width, height, options, decodeCallback);
        } catch (GlideException e) {
            exceptions.add(e);
        }
        if(result ! =null) {
            break; }}if (result == null) {
        throw new GlideException(failureMessage, new ArrayList<>(exceptions));
    }
    return result;
}
//DecodePath#decode
public Resource<Transcode> decode(
    DataRewinder<DataType> rewinder,
    int width,
    int height,
    @NonNull Options options,
    DecodeCallback<ResourceType> callback)
    throws GlideException {
    / / decoding
    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options);
    DecodeJob#onResourceDecoded(DataSource DataSource, @nonnull Resource
      
        decoded) DecodeJob#onResourceDecoded, complete the transform, and determine whether to cache the resources after the transform according to the cache strategy. Cache the transformed resources in ResourceCache so that they can be obtained from ResourceCacheGenerator.)
      
    Resource<ResourceType> transformed = callback.onResourceDecoded(decoded);
    // Transcode the resources applied to the transform. For example, the transformed Resource
      
        is transformed into Resource
       
        .
       
      
    return transcoder.transcode(transformed, options);
}

Copy the code

When the transcoding is complete, the transcoding resource is finally called back to the DrawableImageViewTarget through layer by layer return.

DecodeJob – > EngineJob – > SingleRequest – > DrawableImageViewTarget.

2.5 Cache Module

Glide cache is divided into two types, one is memory cache, the other is disk cache. In the data request module, we have analyzed that the data request is first obtained from the disk cache after decoding and transformation, and then obtained from the source file disk cache if it is not obtained, and then obtained from the source. That section we only analyze the memory cache, after the memory cache analysis, then the whole cache module is clear at a glance, finally through a cache search process to describe the whole Glide cache mechanism.

Memory cache:

If engine#load is not found, it should be searched in the memory cache

public <R> LoadStatus load(...). { EngineKey key =... EngineResource<? > memoryResource;synchronized (this) {
    memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
    if (memoryResource == null) {
      // If there is none in the active cache or memory cache, a new load or reuse of available request tasks is started
      return waitForExistingOrStartNewJob(...);
    }
  }
  // If the active cache or memory cache exists, the callback succeeds
  cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
  return null;
}
privateEngineResource<? > loadFromMemory( EngineKey key,boolean isMemoryCacheable, long startTime) {
    if(! isMemoryCacheable) {return null;
    }
    // Search from the active cacheEngineResource<? > active = loadFromActiveResources(key);if(active ! =null) {
        return active;
    }
    // Search from memory cacheEngineResource<? > cached = loadFromCache(key);if(cached ! =null) {
        return cached;
    }
    return null;
}

Copy the code

The entire memory cache is divided into two phases

  1. Active cache: weak reference storage of Map structures
  2. Memory cache: Storage of the Lru structure

What is an active cache?

The active cache, as its name implies, caches resources on the current page. After the current page is destroyed, the resources in the active cache are removed and added to the in-memory cache. Its purpose is to minimize the number of list lookups from the disk cache. Because the memory cache size is limited, the algorithm used is Lru algorithm, and the old algorithm is eliminated. If you slide back and forth in a list for many times, it will cause the slide out of the screen to be removed. If you slide into the screen again at this time, you need to find the new magnetic disk. The active cache uses weak reference storage of the Map structure to prevent old resources in the current list from being collected unless a garbage collection is triggered. Reduced lookups from the disk cache and improved resource lookups performance.

What is a memory cache?

The memory cache uses storage resources with an Lru structure, which, according to the Lru nature, uses the least recent algorithm. That is, when the cache is full, the recently unused resource is removed. To avoid unlimited cache growth, resulting in OOM.

Now that we’ve explained the definition of active and memory caching, let’s look at the code implementation, starting with the active cache

What does the Map store in the active cache?

final class ActiveResources {
  private final Executor monitorClearedResourcesExecutor;
  final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
  private finalReferenceQueue<EngineResource<? >> resourceReferenceQueue =new ReferenceQueue<>();
  private volatile booleanisShutdown; . ActiveResources(... ,Executor monitorClearedResourcesExecutor) {this.monitorClearedResourcesExecutor = monitorClearedResourcesExecutor;
    monitorClearedResourcesExecutor.execute(
        new Runnable() {
          @Override
          public void run(a) { cleanReferenceQueue(); }}); }}// Store the resource in the active cache
synchronized void activate(Key key, EngineResource
        resource) {
  // Create a ResourceWeakReference wrapper class for the resource
  ResourceWeakReference toPut =
      new ResourceWeakReference(
          key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
  ResourceWeakReference removed = activeEngineResources.put(key, toPut);
   if(removed ! =null) { removed.reset(); }}static final class ResourceWeakReference extends WeakReference<EngineResource<? >>{
    final Key key;
    final booleanisCacheable; Resource<? > resource; ResourceWeakReference( Key key, EngineResource<? > referent, ReferenceQueue<?superEngineResource<? >> queue,boolean isActiveResourceRetentionAllowed) {
      super(referent, queue);
      this.key = Preconditions.checkNotNull(key);
      this.resource =
          referent.isMemoryCacheable() && isActiveResourceRetentionAllowed
              ? Preconditions.checkNotNull(referent.getResource())
              : null;
      isCacheable = referent.isMemoryCacheable();
    }
    void reset(a) {
      resource = null; clear(); }}Copy the code

As can be seen from the above code snippet, it stores the EngineResource weak reference wrapper class ResourceWeakReference. Why is it designed like this? Why not just store EngineResource? If an EngineResource is stored directly in a Map, it is a strong reference. Garbage collection will not be done unless it is clear(), so weak references are used here. One more interesting thing is that when you create a ResourceWeakReference, that is, when you put it into the Map and call ActiveResources#activate(), you pass in a reference queue that exists to detect when it is recycled.

In the ActiveResources constructor, a detection thread is started that calls cleanReferenceQueue(), which loops over and over again, Unless you manually set isShutdown to true, and in the loop resourcereferencequeu #remove is blocking, that is, blocking if there are no elements, and returning if there are elements, when is there an element? When garbage collection is performed, the retrieved element is placed in the ReferenceQueue, at which point remove returns the retrieved element and then removes it from the active cache

  void cleanReferenceQueue(a) {
    while(! isShutdown) {try {
        ResourceWeakReference ref = (ResourceWeakReference) resourceReferenceQueue.remove();
        // Remove the resource from the cache and put the resource into the memory cache
        cleanupActiveReference(ref);
        DequeuedResourceCallback current = cb;
        if(current ! =null) {
          current.onResourceDequeued();
        }
        // End for testing only.
      } catch(InterruptedException e) { Thread.currentThread().interrupt(); }}}Copy the code

So after analyzing the active cache structure, let’s look at finding the cache logic from the active cache

privateEngineResource<? > loadFromActiveResources(Key key) {// Check the active cache to find the resource reference count +1, indicating that there is currently a place in useEngineResource<? > active = activeResources.get(key);if(active ! =null) {
    active.acquire();
  }
  return active;
}
//ActiveResources#get
synchronizedEngineResource<? > get(Key key) { ResourceWeakReference activeRef = activeEngineResources.get(key);if (activeRef == null) {
        return null; } EngineResource<? > active = activeRef.get();If it is collected, it is removed from the map and put into the memory cache
    if (active == null) {
      cleanupActiveReference(activeRef);
    }
    return active;
}
Copy the code

If it is not found in the active cache, it is found in the memory cache

privateEngineResource<? > loadFromCache(Key key) {// Find the resource from Lru, reference count +1, put it into the active cache returnEngineResource<? > cached = getEngineResourceFromCache(key);if(cached ! =null) {
    cached.acquire();
    activeResources.activate(key, cached);
  }
  return cached;
}
Copy the code

To summarize, if Glide cache policy uses default cache policy, then it obtains resources as follows:

  1. Lookup from the active cache, return if present
  2. Retrieves from the memory cache, if any, into the active cache, and returns
  3. Search from disk cache after decoding transformation, return if there is decoding transcoding
  4. Search from the source file disk cache, return decoding – transform – transcode if present
  5. Obtain the resource from the source, cache the resource to the source file cache, decode – transform – transcoding return

3 Glide decoding detailed analysis

3.1 Bitmap basic knowledge

3.1.1 Bitmap memory usage Calculation

There are two ways to calculate the memory footprint of a bitmap

  • getByteCount()
  • getAllocationByteCount()

The differences are as follows:

  1. The two are generally equal
  2. The image is decoded by multiplexing the Bitmap. If the memory of the multiplexed Bitmap is larger than that of the Bitmap to be allocated, then getByteCount() represents the memory occupied by the newly decoded image (not the actual memory size, the actual size is the size of the multiplexed Bitmap). GetAllocationByteCount () indicates the actual memory used by the overused Bitmap.
  3. After Android4.4(API 19),getByteCount() is always less than or equal to getAllocationByteCount() by reusing the decoded Bitmap.

Decode a picture from different drawable under RES, occupy memory calculation


w i d t h x h e i g h t x t a r g e t D e n s i t y present i n D e n s i t y x t a r g e t D e n s i t y present i n D e n s i t y x The amount of memory taken up by a pixel Width \times height\times targetDensity \div inDensity \times targetDensity \div inDensity \times The memory occupied by a pixel
  • TargetDensity Cell phone pixel density
  • InDensity Indicates the pixel density under different drawable
  • The amount of memory taken up by a pixel
    1. ALPHA_8 — (1B)
    2. RGB_565 — (2B)
    3. ARGB_4444 — (2B)
    4. ARGB_8888 — (4B)
    5. RGBA_F16 — (8B)

For example, loading a 100 × 100 image on a phone with 480 pixel density would be ARGB_8888 if the default encoding is used

  • When the picture is placed under xxHDPI, the memory of the decoded picture is


    100 x 100 x 480 present 480 x 480 present 480 x 4 = 40000 B material 39 K 100\times100\times480\div480\times480\div480\times4=40000B\approx39K
  • When the picture is placed under XHDPI, the memory occupied by the decoded picture is


    100 x 100 x 480 present 320 x 480 present 320 x 4 = 90000 B material 88 K 100\times100\times480\div320\times480\div320\times4=90000B\approx88K

Other bitmapFactory. decodeXxx occupies memory:


w i d t h x h e i g h t x The amount of memory taken up by a pixel Width \times height \times The memory occupied by a pixel

3.1.2 Bitmap memory model

Before Android 2.3.3 (API10), pixel data of Bitmap was stored in Native memory, while Bitmap objects themselves were stored in Dalvik Heap. After Android3.0, Bitmap pixel data is also placed in the Dalvik Heap. Since Android 8.0, Bitmap pixel data has been put into Native memory.

3.1.3 Bitmap Memory Management

Recycle () is recommended for Android 2.3.3 and later.

You should use recycle() only if you are sure that the bitmap is no longer in use. If you call recycle() and later try to draw a bitmap, you get an error: “Canvas: Trying to use a recycled bitmap”.

Manage memory on Android 3.0 and later

Android3.0 (API) after 11 BitmapFactory is introduced. The Options. InBitmap fields, set this field after decoding method will try to set the Bitmap memory multiplexing inBitmap field. This means that Bitmap memory is overused, avoiding the process of memory reclamation and requisition, which is obviously better performance. However, there are several limitations to using this field:

The limitation of inBitmap

  • Bitmaps that can be reused must be set to true;

  • Before Android4.4(API 19), only JPG, PNG, same width and height (demanding), inSampleSize 1 Bitmap can be reused

  • The inPreferredConfig of the Bitmap reused before Android4.4(API 19) overwrites the inPreferredConfig of the Bitmap setting to allocate memory

  • The memory of the Bitmap to be reused after Android4.4(API 19) must be larger than the memory of the Bitmap to be allocated

  • The options. inSampleSize of the Bitmap to be loaded on Android4.4(API 19) must be explicitly specified as 1

3.1.4 Bitmap Basic Knowledge Reference Materials

www.jianshu.com/p/3c5ac5fdb…

www.jianshu.com/p/e49ec7d05…

3.2 Detailed analysis of decoding

Loading both local and network images is basically done by decode() of the five parameters in Downsampler, so the logic in decode will be analyzed in detail next.

private Resource<Bitmap> decodeImageReader ImageReader,// to get the image type, and the direction; And bitmap decodingintRequestedWidth, / / the view wideintDecodeCallbacks, DecodeCallbacks)
      throws IOException {
    byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
    BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
    bitmapFactoryOptions.inTempStorage = bytesForOptions;
    // Get the default decoder format, PREFER_ARGB_8888, or change the default with builder.setdecodeformat (decodeformat.rgb_565)
    DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
    // Get the target color space, 8.0 is SRGB, 9.0 or higher, DISPLAY_P3 can be selected, default is SRGB
    //https://juejin.cn/post/6844903865381289997
    PreferredColorSpace preferredColorSpace = options.get(PREFERRED_COLOR_SPACE);
    // get the sampling strategy, which defaults to DownsampleStrategy#FIT_CENTER, depending on the scaleType set by your ImageView
    DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION);
    // The size of the bitmap matches the width and height of the resource request. The default is false
    boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);
    // Whether hardware bitmaps are allowed
    //https://muyangmin.github.io/glide-docs-cn/doc/hardwarebitmaps.html 
    booleanisHardwareConfigAllowed = options.get(ALLOW_HARDWARE_CONFIG) ! =null && options.get(ALLOW_HARDWARE_CONFIG);

    try {
      // Sampling and reuse are done in decodeFromWrappedStreams
      Bitmap result =
          decodeFromWrappedStreams(
              imageReader,
              bitmapFactoryOptions,
              downsampleStrategy,
              decodeFormat,
              preferredColorSpace,
              isHardwareConfigAllowed,
              requestedWidth,
              requestedHeight,
              fixBitmapToRequestedDimensions,
              callbacks);
      return BitmapResource.obtain(result, bitmapPool);
    } finally{ releaseOptions(bitmapFactoryOptions); byteArrayPool.put(bytesForOptions); }}private Bitmap decodeFromWrappedStreams(
      ImageReader imageReader,
      BitmapFactory.Options options,
      DownsampleStrategy downsampleStrategy,
      DecodeFormat decodeFormat,
      PreferredColorSpace preferredColorSpace,
      boolean isHardwareConfigAllowed,
      int requestedWidth,
      int requestedHeight,
      boolean fixBitmapToRequestedDimensions,
      DecodeCallbacks callbacks)
      throws IOException {  
    // Get the image width and height
    int[] sourceDimensions = getDimensions(imageReader, options, callbacks, bitmapPool);
    // Get the image width
    int sourceWidth = sourceDimensions[0];
    // Get the image height
    int sourceHeight = sourceDimensions[1];
    // Get the image media type image/ JPEGString sourceMimeType = options.outMimeType; .// Get the direction of the image
    int orientation = imageReader.getImageOrientation();
    // Get the image rotation Angle
    int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
    boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation);
    // Adjust the width and height of the request based on whether the original image size is obtained and whether the image is rotated
    int targetWidth =
        requestedWidth == Target.SIZE_ORIGINAL
            ? (isRotationRequired(degreesToRotate) ? sourceHeight : sourceWidth)
            : requestedWidth;
    int targetHeight =
        requestedHeight == Target.SIZE_ORIGINAL
            ? (isRotationRequired(degreesToRotate) ? sourceWidth : sourceHeight)
            : requestedHeight;
    // Get the image format JPEG
    ImageType imageType = imageReader.getImageType();
    // Calculate the inSampleSize setting, which will be examined in more detail below
    calculateScaling(
        imageType,
        imageReader,
        callbacks,
        bitmapPool,
        downsampleStrategy,
        degreesToRotate,
        sourceWidth,
        sourceHeight,
        targetWidth,
        targetHeight,
        options);
    // Configure the bitmapFactory. Options inPreferredConfig parameter
    / / inPreferredConfig description: https://blog.csdn.net/ccpat/article/details/46834089
    calculateConfig(
        imageReader,
        decodeFormat,
        isHardwareConfigAllowed,
        isExifOrientationRequired,
        options,
        targetWidth,
        targetHeight);

    boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
      int expectedWidth;
      int expectedHeight;
      if (sourceWidth >= 0
          && sourceHeight >= 0
          && fixBitmapToRequestedDimensions
          && isKitKatOrGreater) {
        expectedWidth = targetWidth;
        expectedHeight = targetHeight;
      } else {
        // According to calculateScaling, isScaling(options) is false, where densityMultiplier=1f
        float densityMultiplier =
            isScaling(options) ? (float) options.inTargetDensity / options.inDensity : 1f;
        int sampleSize = options.inSampleSize;
        //downsampledHeigh=1000/2  
        int downsampledWidth = (int) Math.ceil(sourceWidth / (float) sampleSize);
        //downsampledHeigh=500=1000/2  
        int downsampledHeight = (int) Math.ceil(sourceHeight / (float) sampleSize);
        //expectedWidth=500
        expectedWidth = Math.round(downsampledWidth * densityMultiplier);
        //expectedWidth=500
        expectedHeight = Math.round(downsampledHeight * densityMultiplier);
      }
     
      if (expectedWidth > 0 && expectedHeight > 0) {
        ExpectedWidth and expectedHeight from the bitmap pool can be reused
        // set it to options.inBitmapsetInBitmap(options, bitmapPool, expectedWidth, expectedHeight); }}// Set the target color space, 8.0 is SRGB, 9.0 or higher can select DISPLAY_P3, default is SRGB
    //https://juejin.cn/post/6844903865381289997
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
      booleanisP3Eligible = preferredColorSpace == PreferredColorSpace.DISPLAY_P3 && options.outColorSpace ! =null
              && options.outColorSpace.isWideGamut();
      options.inPreferredColorSpace =
          ColorSpace.get(isP3Eligible ? ColorSpace.Named.DISPLAY_P3 : ColorSpace.Named.SRGB);
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
    }
    / / decoding
    Bitmap downsampled = decodeStream(imageReader, options, callbacks, bitmapPool);
    // call back to transcodecallbacks.onDecodeComplete(bitmapPool, downsampled); .return rotated;
}
Copy the code

CalculateScaling analysis

  private static void calculateScaling(
      ImageType imageType,
      ImageReader imageReader,
      DecodeCallbacks decodeCallbacks,
      BitmapPool bitmapPool,
      DownsampleStrategy downsampleStrategy,
      int degreesToRotate,
      int sourceWidth,
      int sourceHeight,
      int targetWidth,
      int targetHeight,
      BitmapFactory.Options options)
      throws IOException {
 
    int orientedSourceWidth = sourceWidth;
    int orientedSourceHeight = sourceHeight;
 
    // Calculate scaling factors, such as images
    /** DownsampleStrategy#FIT_CENTER float widthPercentage = requestedWidth / (float) sourceWidth; float heightPercentage = requestedHeight / (float) sourceHeight; return Math.min(widthPercentage, heightPercentage); * /
    // If the width of the image is 1000 and the height is 1000, the requested width and height are 500*500
    // Calculate 0.5
    final float exactScaleFactor =
        downsampleStrategy.getScaleFactor(
            orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight);

    //SampleSizeRounding.QUALITY
    SampleSizeRounding rounding =
        downsampleStrategy.getSampleSizeRounding(
            orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight);

    / / outWidth = 0.5 * 1000 = 500
    int outWidth = round(exactScaleFactor * orientedSourceWidth);
    / / outHeight = 0.5 * 1000 = 500
    int outHeight = round(exactScaleFactor * orientedSourceHeight);
    //widthScaleFactor=2=1000/500
    int widthScaleFactor = orientedSourceWidth / outWidth;
    //heightScaleFactor=2=1000/500 
    int heightScaleFactor = orientedSourceHeight / outHeight;

    //scaleFactor=2
    int scaleFactor =
        rounding == SampleSizeRounding.MEMORY
            ? Math.max(widthScaleFactor, heightScaleFactor)
            : Math.min(widthScaleFactor, heightScaleFactor);

    int powerOfTwoSampleSize;
    // BitmapFactory does not support downsampling wbmp files on platforms <= M. See b/27305903.
    if (Build.VERSION.SDK_INT <= 23
        && NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) {
      powerOfTwoSampleSize = 1;
    } else {
      //powerOfTwoSampleSize=2 integer. highestOneBit(scaleFactor) Specifies the highest binary bit
      powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor));
      if (rounding == SampleSizeRounding.MEMORY
          && powerOfTwoSampleSize < (1.f / exactScaleFactor)) {
        powerOfTwoSampleSize = powerOfTwoSampleSize << 1; }}//inSampleSize=2
    options.inSampleSize = powerOfTwoSampleSize;
    int powerOfTwoWidth;
    int powerOfTwoHeight;
    // The image we loaded is a JPEG
    if (imageType == ImageType.JPEG) {
      // libjpegturbo can downsample up to a sample size of 8. libjpegturbo uses ceiling to round.
      // After libjpegturbo's native rounding, skia does a secondary scale using floor
      // (integer division). Here we replicate that logic.
      / / 2
      int nativeScaling = Math.min(powerOfTwoSampleSize, 8);  
      / / 500
      powerOfTwoWidth = (int) Math.ceil(orientedSourceWidth / (float) nativeScaling);
      / / 500
      powerOfTwoHeight = (int) Math.ceil(orientedSourceHeight / (float) nativeScaling);
      / / 0
      int secondaryScaling = powerOfTwoSampleSize / 8;
      if (secondaryScaling > 0) { powerOfTwoWidth = powerOfTwoWidth / secondaryScaling; powerOfTwoHeight = powerOfTwoHeight / secondaryScaling; }}else if (imageType == ImageType.PNG || imageType == ImageType.PNG_A) {
      powerOfTwoWidth = (int) Math.floor(orientedSourceWidth / (float) powerOfTwoSampleSize);
      powerOfTwoHeight = (int) Math.floor(orientedSourceHeight / (float) powerOfTwoSampleSize); }...//adjustedScaleFactor=1
    double adjustedScaleFactor =
        downsampleStrategy.getScaleFactor(
            powerOfTwoWidth, powerOfTwoHeight, targetWidth, targetHeight);
      
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      //inDensity = Integer.MAX_VALUE  
      options.inTargetDensity = adjustTargetDensityForError(adjustedScaleFactor);
      //inDensity = Integer.MAX_VALUE  
      options.inDensity = getDensityMultiplier(adjustedScaleFactor);
    }
    if (isScaling(options)) {
      options.inScaled = true;
    } else {
       // Else since inTargetDensity and inDensity are equal
      options.inDensity = options.inTargetDensity = 0; }}Copy the code

We went full circle, mainly using the system-provided inSampleSize to change the size of the original image to reduce the size of the bitmap loaded into memory.

So after analyzing Glide decoding code, I think Glide has made the following optimization on picture decoding

  1. InSampleSize to change the size of the original image to reduce the size of the bitmap loaded into memory

  2. Reasonable inPreferredConfig Settings. (preferredConfig (builder.setdecodeformat (preferdeformat.rGB_565))); For example, if you set RGB_565 to load an image with transparency, Glide will automatically change it to bitmap.config.argb_8888 for you. As for why see inPreferredConfig description: blog.csdn.net/ccpat/artic…

  3. InBitmap is used to reuse the existing unused Bitmap to reduce memory application and release

  4. An ArrayPool is used to duplicate byte arrays used as temporary space for decoding images from InputStream to reduce memory requirment and freeing

  5. Determine whether to use hardware bitmap based on the version (Android8.0 or above) and whether to enable hardware bitmap. Note that enabling hardware bitmap means that bitmap cannot be reused. Muyangmin. Making. IO/glide – docs -…

    Determine whether to start hardware bitmap code

    //DecodeJob#getOptionsWithHardwareConfig    
    private Options getOptionsWithHardwareConfig(DataSource dataSource) {
        Options options = this.options;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
          return options;
        }
        
        boolean isHardwareConfigSafe =
            dataSource == DataSource.RESOURCE_DISK_CACHE || decodeHelper.isScaleOnlyOrNoTransform();
        Boolean isHardwareConfigAllowed = options.get(Downsampler.ALLOW_HARDWARE_CONFIG);
    
        // If allow hardware config is defined, we can use it if it's set to false or if it's safe to
        // use the hardware config for the request.
        if(isHardwareConfigAllowed ! =null&& (! isHardwareConfigAllowed || isHardwareConfigSafe)) {return options;
        }
    
        // If allow hardware config is undefined or is set to true but it's unsafe for us to use the
        // hardware config for this request, we need to override the config.
        options = new Options();
        options.putAll(this.options);
        options.set(Downsampler.ALLOW_HARDWARE_CONFIG, isHardwareConfigSafe);
       
        return options;
      }
    Copy the code

4 Transform Process analysis

In the previous analysis of Glide decoding and transcoding module, when resources are decoded by the callback. OnResourceDecoded (decoded); Call back to DecodeJob#onResourceDecoded to complete the transform. Here is a detailed analysis of the transform process.

The resource is decoded and the code snippet is called back

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

DecodeJob#onResourceDecoded code snippet

/** DataSource is an enumeration type. LOCAL: resources are retrieved from the LOCAL. REMOTE: resources are retrieved from the REMOTE (network) DATA_DISK_CACHE: resources are retrieved from the source file disk cache. RESOURCE_DISK_CACHE: MEMORY_CACHE is retrieved from the disk cache after transforme Retrieving decoded from the memory cache represents decoded resources */
<Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
    Class<Z> resourceSubClass = (Class<Z>) decoded.get().getClass();
    Transformation<Z> appliedTransformation = null;
    Resource<Z> transformed = decoded;
    // If it is not retrieved from the disk cache after the transform, perform the transform.
    if(dataSource ! = DataSource.RESOURCE_DISK_CACHE) {// Find the specific transform processing class according to the decoding type
      appliedTransformation = decodeHelper.getTransformation(resourceSubClass);
      // Perform the transform operation and return the transformed resource
      transformed = appliedTransformation.transform(glideContext, decoded, width, height);
    }
    // If the decoded resource is not the same as the transformed resource, the decoded resource is reclaimed
    if(! decoded.equals(transformed)) { decoded.recycle(); }// Encoding policy used for disk caching of transformed resources
    final EncodeStrategy encodeStrategy;
    final ResourceEncoder<Z> encoder;
    // Determine whether there is a corresponding encoder according to the transformed resources
    if (decodeHelper.isResourceEncoderAvailable(transformed)) {  
      // If so, obtain the corresponding encoder. If the transformed resource is BitmapResource, then the encoder is BitmapEncoder
      encoder = decodeHelper.getResultEncoder(transformed);
      / / depending on the encoder for coding strategy, if is BitmapEncoder here got the EncodeStrategy TRANSFORMED
      encodeStrategy = encoder.getEncodeStrategy(options);
    } else {
      encoder = null;
      encodeStrategy = EncodeStrategy.NONE;
    }
    Resource<Z> result = transformed;
    booleanisFromAlternateCacheKey = ! decodeHelper.isSourceKey(currentSourceKey);/ / based on cache strategy can determine whether the cache after the transformation of resources, the default caching policy for DiskCacheStrategy. AUTOMATIC
    if (diskCacheStrategy.isResourceCacheable(
        isFromAlternateCacheKey, dataSource, encodeStrategy)) {
      if (encoder == null) {
        throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass());
      }
      final Key key;
      switch (encodeStrategy) {
        case SOURCE:
          key = new DataCacheKey(currentSourceKey, signature);
          break;
        case TRANSFORMED:
          // Get the cache key of the transformed resource disk
          key =
              new ResourceCacheKey(
                  decodeHelper.getArrayPool(),
                  currentSourceKey,
                  signature,
                  width,
                  height,
                  appliedTransformation,
                  resourceSubClass,
                  options);
          break;
        default:
          throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy);
      }
      // Package the resources after the transformation,
      LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
      // Pass the cache key to the DecodeJob class DeferredEncodeManager, and then call back to the EngineJob to complete the cache of the changed resource. The transformed resources are cached in the ResourceCache layer.
      deferredEncodeManager.init(key, encoder, lockedResult);
      result = lockedResult;
    }
    return result;
  }
Copy the code

Long winded so much, it is important to complete the converter on these two lines of code after the search and decoding of resources transformed into transformed resources

// Find the specific transform processing class according to the resource type decoded
appliedTransformation = decodeHelper.getTransformation(resourceSubClass);
// Perform the transform operation and return the transformed resource
transformed = appliedTransformation.transform(glideContext, decoded, width, height);
Copy the code

You can look up a Map of mineralizations or values in DecodeHelper, or you can look up a Map of mineralizations or values anywhere you want. You can look up a Map of mineralizations or values anywhere you want to look.

By tracing code calls from RequestBuilder layer by layer, we add them when we call into, based on ImageView#scaleType which transformation operations need to be added. ImageView’s default scaleType is FIT_CENTER, so optionalFitCenter() is triggered. BaseRequestOptions#transform() is called.

//BaseRequestOptions#transform  
T transform(@NonNull Transformation<Bitmap> transformation, boolean isRequired) {
    if (isAutoCloneEnabled) {
      return clone().transform(transformation, isRequired);
    }
    // ImageView scaleType is FIT_CENTER, so transformation is FitCenter
    DrawableTransformation drawableTransformation =
        new DrawableTransformation(transformation, isRequired);
    // Add the FitCenter to the transformations
    transform(Bitmap.class, transformation, isRequired);
    transform(Drawable.class, drawableTransformation, isRequired);
    transform(BitmapDrawable.class, drawableTransformation.asBitmapDrawable(), isRequired);
    transform(GifDrawable.class, new GifDrawableTransformation(transformation), isRequired);
    return selfOrThrowIfLocked();
  }
Copy the code

Glide automatically registers four Transforms when calling into based on the code analysis above.

Go back to find the converter entry code

// Find the specific transform processing class according to the resource type decoded
appliedTransformation = decodeHelper.getTransformation(resourceSubClass);
Copy the code

If the resourceSubClass type is Bitmap, FitCenter is found, and FitCenter#transform is called.

The FitCenter inheritance system is as follows

If you only need to transform a Bitmap, it’s best to inherit a BitmapTransformation. BitmapTransformation handles some of the basics for us, for example, if your transformation returns a newly modified Bitmap, BitmapTransformation takes care of extracting and recycling the original Bitmap

For details about how to customize Transformation, please refer to the Chinese Glide document:

Muyangmin. Making. IO/glide – docs -…