About the author

Guo Xiaoxing, programmer and guitarist, is mainly engaged in the infrastructure of Android platform. Welcome to exchange technical questions. You can go to my Github to raise an issue or send an email to [email protected] to communicate with me.

The article directories

  • Picture loading process
    • 1.1 Initializing Fresco
    • 1.2 get a DataSource
    • 1.3 Binding DraweeController to DraweeHierarchy
    • 1.4 Get the image from memory cache/disk cache/network and set it to the corresponding Drawable layer
  • Two DraweeController DraweeHierarchy
    • 2.1 Layer hierarchy construction
    • 2.2 Layer construction process
  • Three Producer and Consumer
  • 4 cache Mechanism
    • 3.1 Memory Cache
    • 3.2 Disk Caching

For more Android open Framework source analysis articles, see Android Open Framework Analysis.

This series of articles originally called “Android open source framework source code analysis”, later these excellent open source library code to see much, feel the big men code to write the true beauty of the picturesque 👍, so it was renamed “Android open source framework source appreciation”. Without further ado, the open source library for today’s analysis is Fresco.

Fresco is a full-featured image loading framework that is widely used in Android development. What makes Fresco so popular as an image loading framework?

  • Perfect memory management function, reduce the image to the memory, even in the low-end machine has a good performance.
  • You can customize the image loading process to display a low definition image or thumbnail image first, and then display a high definition image after loading. You can scale and rotate the image during loading.
  • Custom image drawing process, you can customize valley focus, rounded corner map, placeholder map, overlay, drawing bar.
  • Progressive display of images.
  • Support the Gif.
  • Support Webp.

Ok, another wave of Fresco, but it’s no use just knowing how good he is. We need to know why he is so good and how he achieved it. Fresco source code is still more, it seems to be a bit of effort, but not afraid, Android system source code has been chewed down by us, but also afraid of a small Fresco 😎? To better understand the Fresco implementation, it is important to understand its modules and layers as a whole, one by one.

Since Fresco is quite large, let’s take a look at its overall structure to get a sense of it.

👉 Click on the image for a larger version

  • DraweeView: inherited from ImageView, it simply reads some attribute values of XML files and does some initialization work. Hierarchy is responsible for layer management and layer data acquisition.
  • DraweeHierarchy: Consists of multiple layers of drawables, each of which provides some function (for example: zoom, rounded corners).
  • DraweeController: controls data acquisition and image loading, sends requests to pipeline and receives corresponding events, controls Hierarchy according to different events, receives user events from DraweeView, and then performs operations such as canceling network requests and recovering resources.
  • DraweeHolder: Coordinates management of Hierarchy and DraweeHolder.
  • ImagePipeline: The core Fresco module for capturing images in a variety of ways (memory, disk, network, etc.).
  • Producer/Consumer: There are many kinds of Producer. Producer is used to complete network data acquisition, cache data acquisition, picture decoding and other works. The results produced by Producer are consumed by consumers.
  • IO/Data: This layer is the Data layer, which implements memory caching, disk caching, network caching, and other IO related functions.

Throughout the Fresco architecture, DraweeView is the facade that interacts with the user, DraweeHierarchy is the view hierarchy that manages layers, and DraweeController is the controller that manages data. They form the troika of the Fresco framework. Of course, there is our behind-the-scenes hero Producer, who does all the dirty work. The best model 👍

As we understand the overall Fresco architecture, we also understand the key players that play an important role in the mine, as follows:

  • Supplier: Provides a specific type of object. There are many classes in Fresco that end in Supplier that implement this interface.
  • SimpleDraweeView: This one is familiar, it takes a URL and calls Controller to load the image. This class inherits from GenericDraweeView, which in turn inherits from DraweeView, the top Fresco View class.
  • PipelineDraweeController: responsible for image data acquisition and load, it is inherited from AbstractDraweeController, by PipelineDraweeControllerBuilder construct. AbstractDraweeController implements the DraweeController interface, which is the data manager for Fresco.
  • GenericDraweeHierarchy: Responsible for layer management on SimpleDraweeView. It consists of multiple layers of drawables, each of which provides some function (e.g. Zoom, rounded corners), the class by GenericDraweeHierarchyBuilder build, PlaceholderImage, retryImage, failureImage, progressBarImage, background, overlays, pressedStateOverlay, etc Attribute information set in XML files or Java code is passed to GenericDraweeHierarchy for processing.
  • DraweeHolder: This class is a Holder class associated with SimpleDraweeView, which is managed through DraweeHolder. DraweeHolder is used for unified management of the Hierarchy and Controller
  • DataSource: DataSource is similar to Java Futures, which represents the source of data.
  • DataSubscriber: receives the result returned by the DataSource.
  • ImagePipeline: Interface for retrieving images.
  • Producer: loading and processing images, it has multiple implementations, such as: NetworkFetcherProducer, LocalAssetFetcherProducer, LocalFileFetchProducer. The names of these classes tell us what they do. The Producer is built by the ProducerFactory class, and all the producers are like Java I/O streams, which can be nested one layer at a time and only get one result. This is a very clever design 👍
  • Consumer: is used to receive the results produced by Producer, which, along with Producer, forms the Producer and Consumer model.

Note: The class names in the Fresco source code are long, but they follow certain command patterns, such as: Classes that end in Supplier implement the Supplier interface, which provides objects of a certain type (Factory, Generator, Builder, Closure, etc.). A class that ends in Builder is, of course, a class that creates objects in constructor mode.

From the above description, you should have an overall understanding of Fresco. What should we focus on when analyzing such a large library? 🤔

  1. Image loading process
  2. DraweeController and DraweeHierarchy
  3. Producer and Consumer
  4. Caching mechanisms

👉 Note: Fresco also uses a wide variety of design patterns, including Builder, Factory, Wrapper, Producer/Consumer, Adapter, etc.

Let’s take these four questions to the source code.

Picture loading process

We’ll start with a simple example of how Fresco loads images, then analyze how it loads images to get a sense of the whole process, and then break down the implementation of each of Fresco’s sub-modules.

Ok, so let’s start with a little example.

👉 example

Initialize the

Fresco.initialize(this);
Copy the code

Loading pictures

String url = "https://github.com/guoxiaoxing/android-open-framwork-analysis/raw/master/art/fresco/scenery.jpg";
SimpleDraweeView simpleDraweeView = findViewById(R.id.drawee_view);
simpleDraweeView.setImageURI(Uri.parse(url));
Copy the code

Let’s take a look at its call flow, as shown in the sequence diagram below:

👉 Click on the image for a larger version

Well, the diagram looks a little big, but it doesn’t matter, we’ve broken the process into four big steps by color:

  1. Initialize Fresco.
  2. Access to the DataSource.
  3. Bind Controller and Hierarchy.
  4. Get the image from memory cache/disk cache/network and set it to the corresponding Drawable layer.

👉 Note: The Fresco classes are based on interfaces and Abstract classes, and each module has its own set of inheritance, so the process is relatively simple once you understand their inheritance relationships and the relationships between different modules.

Due to the specific details of sequence diagram design, we provide a new summary flow chart for understanding, as shown below:

👉 Click on the image for a larger version

Next, we will analyze them one by one in combination with specific details.

1.1 Initializing Fresco

👉 Sequence Diagram 1.1 -> 1.11

public class Fresco {
    public static void initialize( Context context, @Nullable ImagePipelineConfig imagePipelineConfig, @Nullable DraweeConfig draweeConfig) {
      / /... Repeat initialization check
      try {
        //1. Load the so library, such as giflib, libjpeg, libpng, etc.
        // It is mainly used to decode images.
        SoLoader.init(context, 0);
      } catch (IOException e) {
        throw new RuntimeException("Could not initialize SoLoader", e);
      }
      //2. Set the magePipelineConfig configuration parameter.
      context = context.getApplicationContext();
      if (imagePipelineConfig == null) {
        ImagePipelineFactory.initialize(context);
      } else {
        ImagePipelineFactory.initialize(imagePipelineConfig);
      }
      //3. Initialize SimpleDraweeView.
      initializeDrawee(context, draweeConfig);
    }
  
    private static void initializeDrawee( Context context, @Nullable DraweeConfig draweeConfig) {
      / / build PipelineDraweeControllerBuilderSupplier object and to SimpleDraweeView.
      sDraweeControllerBuilderSupplier =
          newPipelineDraweeControllerBuilderSupplier(context, draweeConfig); SimpleDraweeView.initialize(sDraweeControllerBuilderSupplier); }}Copy the code

You can see that Fresco does three things during initialization:

  1. Load the so library, which is mainly some third party native libraries, such as giflib, libjpeg, libpng, mainly used for decoding images.
  2. Sets the configuration parameter magePipelineConfig passed in.
  3. Initialize SimpleDraweeView.

There are three things we need to focus on:

  • ImagePipelineConfig: Set the ImagePipeline parameter.
  • Provide DraweeControllerBuilder used to construct DraweeController DraweeControllerBuilderSupplier:.

Let’s start with ImagePipelineConfig. ImagePipelineConfig builds the parameters passed to ImagePipeline using builder mode, as shown below:

  • Bitmap.Config mBitmapConfig; Picture quality.
  • Supplier mBitmapMemoryCacheParamsSupplier; Provider of configuration parameters for the memory cache.
  • CountingMemoryCache.CacheTrimStrategy mBitmapMemoryCacheTrimStrategy; Memory cache reduction strategy.
  • CacheKeyFactory mCacheKeyFactory; Create factory for CacheKey.
  • Context mContext; Context.
  • boolean mDownsampleEnabled; Whether to enable image downsampling.
  • FileCacheFactory mFileCacheFactory; Disk cache creation factory.
  • Supplier mEncodedMemoryCacheParamsSupplier; Undecoded picture cache configuration parameter provider.
  • ExecutorSupplier mExecutorSupplier; Thread pool provider.
  • ImageCacheStatsTracker mImageCacheStatsTracker; Image cache status tracker.
  • ImageDecoder mImageDecoder; Picture decoder.
  • Supplier mIsPrefetchEnabledSupplier; Whether preloading is enabled.
  • DiskCacheConfig mMainDiskCacheConfig; Disk cache configuration.
  • MemoryTrimmableRegistry mMemoryTrimmableRegistry; The memory change listening registry, to which objects that need to listen for system memory changes need to be added.
  • NetworkFetcher mNetworkFetcher; Download online pictures, the default use the built-in HttpUrlConnectionNetworkFetcher, can also be customized.
  • PlatformBitmapFactory mPlatformBitmapFactory; The main difference is the location of the Bitmap in memory. Android 5.0 or below is stored in Ashmem, Android 5.0 or above is stored in the Java Heap.
  • PoolFactory mPoolFactory; Build factories for various pools such as Bitmap pools.
  • ProgressiveJpegConfig mProgressiveJpegConfig; Progressive JPEG configuration.
  • Set mRequestListeners; A collection of request listeners that listen for various events during the request process.
  • boolean mResizeAndRotateEnabledForNetwork; Whether to enable compression and rotation of network images.
  • DiskCacheConfig mSmallImageDiskCacheConfig; Disk Cache configuration
  • ImageDecoderConfig mImageDecoderConfig; Picture decoding configuration
  • ImagePipelineExperiments mImagePipelineExperiments; Experimental features of Image Pipe provided by Fresco.

The above parameters basically do not need to be manually configured, unless there is a requirement of customization on the project.

We can find that at the end of the initialization method invocation initializeDrawee () give SimpleDraweeView introduced into a PipelineDraweeControllerBuilderSupplier, this is a very important object, So let’s see what it initializes.

public class PipelineDraweeControllerBuilderSupplier implements
    Supplier<PipelineDraweeControllerBuilder> {
    
      public PipelineDraweeControllerBuilderSupplier( Context context, ImagePipelineFactory imagePipelineFactory, Set
       
         boundControllerListeners, @Nullable DraweeConfig draweeConfig)
        {
        mContext = context;
        / / 1. Obtain ImagePipeline
        mImagePipeline = imagePipelineFactory.getImagePipeline();
    
        if(draweeConfig ! =null&& draweeConfig.getPipelineDraweeControllerFactory() ! =null) {
          mPipelineDraweeControllerFactory = draweeConfig.getPipelineDraweeControllerFactory();
        } else {
          mPipelineDraweeControllerFactory = new PipelineDraweeControllerFactory();
        }
        / / 2. To obtain PipelineDraweeControllerFactory with initialization.mPipelineDraweeControllerFactory.init( context.getResources(), DeferredReleaser.getInstance(), imagePipelineFactory.getAnimatedDrawableFactory(context), UiThreadImmediateExecutorService.getInstance(), mImagePipeline.getBitmapMemoryCache(), draweeConfig ! =null
                ? draweeConfig.getCustomDrawableFactories()
                : null, draweeConfig ! =null
                ? draweeConfig.getDebugOverlayEnabledSupplier()
                : null); mBoundControllerListeners = boundControllerListeners; }}Copy the code

You can see that two important objects are initialized in this method:

  1. Obtain ImagePipeline.
  2. To obtain PipelineDraweeControllerFactory with the initialization.

This PipelineDraweeControllerFactory is used to construct PipelineDraweeController, PipelineDraweeController (PipelineDraweeController) is derived from AbstractDraweeController and is used to control image data fetching and loading. The PipelineDraweeControllerFactory init () () method is the traversal in the parameters into the PipelineDraweeControllerFactory, for preparing building PipelineDraweeController. Let’s see what it’s passing in.

  • Context.getresources () : Android Resources object.
  • DeferredReleaser. GetInstance () : delayed release resources, such as the main thread after processing the message to recycle.
  • MImagePipeline. GetBitmapMemoryCache () : a decoded image cache.

👉 Note: the so-called pull out the radish with mud, in the analysis of picture loading process will inevitably bring in a variety of classes, if the relationship between them is not clear, the first step is just to master the overall loading process, we will analyze these classes one by one.

After this method completes calling SimpleDraweeView initizlize () method will PipelineDraweeControllerBuilderSupplier object set into the static object sDraweeContro SimpleDraweeView The entire initialization process is complete in llerBuilderSupplier.

1.2 get a DataSource

👉 Sequence diagram 2.1 -> 2.12

Before we can analyze how to create a DataSource, we need to understand what it is.

DataSource is an interface whose implementation class is AbstractDataSource. It can submit data requests and obtain progress, fail result, success result and other information, similar to Future in Java.

The DataSource interface is as follows:

public interface DataSource<T> {
  // Whether the data source is closed
  boolean isClosed(a);
  // The result of the asynchronous request
  @Nullable T getResult(a);
  // If any results are returned
  boolean hasResult(a);
  // Whether the request ends
  boolean isFinished(a);
  // Whether an error occurred in the request
  boolean hasFailed(a);
  // The cause of the error
  @Nullable Throwable getFailureCause(a);
  // Request progress [0, 1]
  float getProgress(a);
  // End the request and release the resource.
  boolean close(a);
  // Send and subscribe the request and wait for the result of the request.
  void subscribe(DataSubscriber<T> dataSubscriber, Executor executor);
}
Copy the code

AbstractDataSource Implements the DataSource interface, which is a base class that all other DataSource classes extend from. AbstractDataSource implements the methods described above and maintains the success, progress, and fail states of the DataSource. There are also the following DataSource classes:

  • AbstractProducerToDataSourceAdapter: inherited from AbstractDataSource, packing the Producer in data process, is to create a Consumer, the detailed process also behind us.
  • CloseableProducerToDataSourceAdapter: Inherited from AbstractProducerToDataSourceAdapter, realized closeResult () method, draw you destroyed destroyed the Result at the same time, this is the main use of the DataSource.
  • ProducerToDataSourceAdapter: no additional method implementation, only for preloading images.
  • IncreasingQualityDataSource: Internal maintenance a CloseableProducerToDataSourceAdapter list, according to the definition of the data from the back forward increasing, it is the list of each binding a DataSubscriber DataSour test, this class is responsible for guarantee Every time to obtain higher resolution data, Destroy less clear data while capturing it.
  • FirstAvailableDataSource: Maintain a list of CloseableProducerToDataSourceAdapter within, it returns a list of the first to get the data the DataSource, it as a list of each binding a DataSubscriber DataSour test, if the data is loaded successfully, The current successful DataSource is specified as the target DataSource. Otherwise, the next DataSource continues.
  • SettableDataSource: AbstractDataSource inherits from AbstractDataSource and overrides settResult(), setFailure(), setProgress() to call the corresponding function of the parent class internally, but the modifier becomes public (originally protected). Even with a SettableDataSource you can call these three functions externally to set the DataSource state. It is used to generate a DataSource set to Failure if the DataSource fails to be retrieved.

Now that we know about our DataSource, let’s see how it’s made.

When we use Fresco to display images, we simply call setImageURI() to set the URL of the image. We start with this method as follows:

public class SimpleDraweeView extends GenericDraweeView {
    
      public void setImageURI(Uri uri, @Nullable Object callerContext) { DraweeController controller = mSimpleDraweeControllerBuilder .setCallerContext(callerContext) .setUri(uri) .setOldController(getController()) .build(); setController(controller); }}Copy the code

Can be found that SimpleDraweeView transfer outside the URL of the data encapsulation into DraweeController, and call the mSimpleDraweeControllerBuilder constructs a DraweeController object, The DraweeController object is actually PipelineDraweeController.

Let’s take a look at how it is built, by sDraweeControllerBuilderSupplier mSimpleDraweeControllerBuilder call get () method, We already said sDraweeControllerBuilderSupplier is in SimpleDraweeView the initialize () is passed, we continue to see PipelineDraweeController build process.

SimpleDraweeControllerBuilder is calling the superclass AbstractDraweeControllerBuilder for the build () method to build, The build () method, in turn, call its subclasses SimpleDraweeControllerBuilder obtainController () method to accomplish specific subclass SimpleDraweeControllerBuilder build, Let’s look at the implementation.

👉 Note: The Fresco design is a good example of interface oriented programming. Most of the functionality is based on the interface design, and AbstractXXX is designed to encapsulate the general functionality, with specific functionality subclasses implemented.

public class PipelineDraweeControllerBuilder extends AbstractDraweeControllerBuilder<
    PipelineDraweeControllerBuilder.ImageRequest.CloseableReference<CloseableImage>,
    ImageInfo> {
    
      @Override
      protected PipelineDraweeController obtainController(a) {
        DraweeController oldController = getOldController();
        PipelineDraweeController controller;
        // If PipelineDraweeController already exists, reuse it, otherwise build a new PipelineDraweeController.
        if (oldController instanceof PipelineDraweeController) {
          controller = (PipelineDraweeController) oldController;
          controller.initialize(
              obtainDataSourceSupplier(),
              generateUniqueControllerId(),
              getCacheKey(),
              getCallerContext(),
              mCustomDrawableFactories);
        } else {
          controller = mPipelineDraweeControllerFactory.newController(
              obtainDataSourceSupplier(),
              generateUniqueControllerId(),
              getCacheKey(),
              getCallerContext(),
              mCustomDrawableFactories);
        }
        returncontroller; }}Copy the code

If PipelineDraweeController is already available, the PipelineDraweeController can be PipelineDraweeController. Otherwise call PipelineDraweeControllerFactory. NewController () method to build a new PipelineDraweeController. PipelineDraweeControllerFactory. NewController () method is the final call of the construction of the object constructor complete PipelineDraweeController PipelineDraweeController, The rest of the process is simple, focusing on what objects are passed in during the build and how they are generated.

  • ObtainDataSourceSupplier () : Get the data source.
  • GenerateUniqueControllerId () : to generate the Controller ID.
  • GetCacheKey () : obtains the cache key.
  • GetCallerContext () : Gets the context of the caller.
  • ImmutableList list of drawables used to generate various image effects.

The other implementations are relatively simple, but we’ll focus on the obtainDataSourceSupplier() implementation as follows:

public class PipelineDraweeControllerBuilder extends AbstractDraweeControllerBuilder<
    PipelineDraweeControllerBuilder.ImageRequest.CloseableReference<CloseableImage>,
    ImageInfo> {
    
      protected Supplier<DataSource<IMAGE>> obtainDataSourceSupplier() {
        if(mDataSourceSupplier ! =null) {
          return mDataSourceSupplier;
        }
    
        Supplier<DataSource<IMAGE>> supplier = null;
    
        //1. Generate the final image supplier.
        if(mImageRequest ! =null) {
          supplier = getDataSourceSupplierForRequest(mImageRequest);
        } else if(mMultiImageRequests ! =null) {
          supplier = getFirstAvailableDataSourceSupplier(mMultiImageRequests, mTryCacheOnlyFirst);
        }
    
        //2. Generate a Ncreasing -quality supplier, where the supplier has two levels of sharpness.
        if(supplier ! =null&& mLowResImageRequest ! =null) {
          List<Supplier<DataSource<IMAGE>>> suppliers = new ArrayList<>(2);
          suppliers.add(supplier);
          suppliers.add(getDataSourceSupplierForRequest(mLowResImageRequest));
          supplier = IncreasingQualityDataSourceSupplier.create(suppliers);
        }
    
        // If there is no image request, provide an empty supplier.
        if (supplier == null) {
          supplier = DataSources.getFailedDataSourceSupplier(NO_REQUEST_EXCEPTION);
        }
    
        returnsupplier; }}Copy the code

GetDataSourceSupplierForRequest () method is the final call (specific invocation chain can refer to the sequence diagram, there is no longer here) is PipelineDraweeControllerBuilder getDataSourceForRequest ()

public class PipelineDraweeControllerBuilder extends AbstractDraweeControllerBuilder<
    PipelineDraweeControllerBuilder.ImageRequest.CloseableReference<CloseableImage>,
    ImageInfo> {
    
      @Override
      protected DataSource<CloseableReference<CloseableImage>> getDataSourceForRequest(
          ImageRequest imageRequest,
          Object callerContext,
          AbstractDraweeControllerBuilder.CacheLevel cacheLevel) {
        
        // Call ImagePipeline's fetchDecodedImage() method to get the DataSource
        returnmImagePipeline.fetchDecodedImage( imageRequest, callerContext, convertCacheLevelToRequestLevel(cacheLevel)); }}Copy the code

ImagePipeline is the Fresco ImagePipeline entry class. As mentioned earlier, ImagePipeline is the core module of Fresco, which is used to fetch images in various ways (memory, disk, network, etc.).

This is mImagePipeline PipelineDraweeControllerBuilderSupplier invokes the ImagePipelineFactory getImagePipeline () method to create. Let’s look at ImagePipeline’s fetchDecodedImage() method as follows:

public class ImagePipeline {
    
    public DataSource<CloseableReference<CloseableImage>> fetchDecodedImage(
        ImageRequest imageRequest,
        Object callerContext,
        ImageRequest.RequestLevel lowestPermittedRequestLevelOnSubmit) {
      try {
        //1. Get the Producer sequence and provide different data input channels for the DataSource.
        Producer<CloseableReference<CloseableImage>> producerSequence =
            mProducerSequenceFactory.getDecodedImageProducerSequence(imageRequest);
        //2. Call submitFetchRequest() to generate the DataSource.
        return submitFetchRequest(
            producerSequence,
            imageRequest,
            lowestPermittedRequestLevelOnSubmit,
            callerContext);
      } catch (Exception exception) {
        returnDataSources.immediateFailedDataSource(exception); }}}Copy the code

We have already talked about what Producer is.

Producer used for loading and processing images, it has multiple implementations, such as: NetworkFetcherProducer, LocalAssetFetcherProducer, LocalFileFetchProducer. The names of these classes tell us what they do. The Producer is built from the ProducerFactory class, and all producers are nested like Java I/O streams, producing only one result.

We’ll talk more about Producer later. The method does two things:

  1. Getting the Producer sequence provides a variety of data input channels for the DataSource. There are a variety of producers that get picture data from different ways, which we’ll talk about in detail below.
  2. Call the submitFetchRequest() method to generate the DataSource.

You can see that the method ends up calling the submitFetchRequest() method to generate the DataSource, as shown below:

public class ImagePipeline {
    
    private <T> DataSource<CloseableReference<T>> submitFetchRequest(
          Producer<CloseableReference<T>> producerSequence,
          ImageRequest imageRequest,
          ImageRequest.RequestLevel lowestPermittedRequestLevelOnSubmit,
          Object callerContext) {
        final RequestListener requestListener = getRequestListenerForRequest(imageRequest);
    
        try {
          RequestLevel specifies four levels of cache: FULL_FETCH(1) from network or local storage, DISK_CACHE(2) from disk cache, and ENCODED_MEMORY_CACHE(3) from ENCODED_MEMORY_CACHE
          BITMAP_MEMORY_CACHE(4) Decoded memory cache fetch.
          ImageRequest.RequestLevel lowestPermittedRequestLevel =
              ImageRequest.RequestLevel.getMax(
                  imageRequest.getLowestPermittedRequestLevel(),
                  lowestPermittedRequestLevelOnSubmit);
          //2. Encapsulate ImageRequest, RequestListener and other information into SettableProducerContext. ProducerContext is Producer
          ProducerContext can be used to change the state inside the Producer.
          SettableProducerContext settableProducerContext = new SettableProducerContext(
              imageRequest,
              generateUniqueFutureId(),
              requestListener,
              callerContext,
              lowestPermittedRequestLevel,
            /* isPrefetch */ false, imageRequest.getProgressiveRenderingEnabled() || imageRequest.getMediaVariations() ! =null| |! UriUtil.isNetworkUri(imageRequest.getSourceUri()), imageRequest.getPriority());/ / 3. Create a CloseableProducerToDataSourceAdapter, CloseableProducerToDataSourceAdapter is a DataSource.
          return CloseableProducerToDataSourceAdapter.create(
              producerSequence,
              settableProducerContext,
              requestListener);
        } catch (Exception exception) {
          returnDataSources.immediateFailedDataSource(exception); }}}Copy the code

The method does three main things:

  1. RequestLevel specifies four levels of cache: FULL_FETCH(1) from network or local storage, DISK_CACHE(2) from disk cache, ENCODED_MEMORY_CACHE(3) from unconnected memory cache, BITMAP_MEMORY_CACHE(4) Decoded memory cache fetch.
  2. Encapsulate ImageRequest, RequestListener and other information into SettableProducerContext. ProducerContext is the Producer’s context. ProducerContext can be used to change the state inside the Producer.
  3. Create CloseableProducerToDataSourceAdapter CloseableProducerToDataSourceAdapter is a DataSource.

Then CloseableProducerToDataSourceAdapter calls himself the create () method to build a CloseableProducerToDataSourceAdapter object. At this point the DataSource is complete and set up to PipelineDraweeController.

Let’s move on to the process of binding Controller and Hierarchy. 👇

1.3 Binding DraweeController to DraweeHierarchy

👉 Sequence Figure 3.1 -> 3.7

SimpleDraweeView setImageURI() sets up the PipelineDraweeController we built in the SimpleDraweeView setImageURI() method.

  public void setImageURI(Uri uri, @Nullable Object callerContext) {
    DraweeController controller = mSimpleDraweeControllerBuilder
        .setCallerContext(callerContext)
        .setUri(uri)
        .setOldController(getController())
        .build();
    setController(controller);
  }
Copy the code

As can be seen from the sequence diagram above, setController() method is called layer by layer, and the setController() method of DraweeHolder is finally called. DraweeHolder is used to coordinate the management of Controller and Hierarchy. It is a member variable of the DraweeView and is built when the DraweeHolder object is initialized. Let’s look at its setController() method as follows:

public class DraweeHolder<DH extends DraweeHierarchy>
    implements VisibilityCallback {
      public void setController(@Nullable DraweeController draweeController) {
        boolean wasAttached = mIsControllerAttached;
        //1. Detach first if you have already established contact with the Controller.
        if (wasAttached) {
          detachController();
        }
    
        //2. Clear the old Controller.
        if (isControllerValid()) {
          mEventTracker.recordEvent(Event.ON_CLEAR_OLD_CONTROLLER);
          mController.setHierarchy(null);
        }
        
        //3. Reconfigure Hierarchy for Controller.
        mController = draweeController;
        if(mController ! =null) {
          mEventTracker.recordEvent(Event.ON_SET_CONTROLLER);
          mController.setHierarchy(mHierarchy);
        } else {
          mEventTracker.recordEvent(Event.ON_CLEAR_CONTROLLER);
        }
    
        Attach the DraweeHolder and Controller.
        if(wasAttached) { attachController(); }}}Copy the code

The process of the above method is also very simple, as follows:

  1. Detach first if you already have a connection with the Controller.
  2. Know the old Controller.
  3. Resetting Hierarchy for Controller creates a new Controller.
  4. Attach the DraweeHolder and Controller.

There are two key aspects of this process: setting up Hierarchy and attch.

As you can see from the sequence diagram above, the mHierarchy is created by calling inflateHierarchy() in the constructor of the GenricDraweeView, which is actually a GenericDraweeHierarchy object. The setHierarchy() method ends up calling the AbstractDraweeController’s setHierarchy() method as follows:

public abstract class AbstractDraweeController<T.INFO> implements
    DraweeController.DeferredReleaser.Releasable.GestureDetector.ClickListener {
    
      public void setHierarchy(@Nullable DraweeHierarchy hierarchy) {
        / /... logmEventTracker.recordEvent( (hierarchy ! =null)? Event.ON_SET_HIERARCHY : Event.ON_CLEAR_HIERARCHY);//1. Release requests that are currently in progress.
        if (mIsRequestSubmitted) {
          mDeferredReleaser.cancelDeferredRelease(this);
          release();
        }
        //2. Clear existing Hierarchy.
        if(mSettableDraweeHierarchy ! =null) {
          mSettableDraweeHierarchy.setControllerOverlay(null);
          mSettableDraweeHierarchy = null;
        }
        //3. Set the new Hierarchy.
        if(hierarchy ! =null) {
          Preconditions.checkArgument(hierarchy instanceofSettableDraweeHierarchy); mSettableDraweeHierarchy = (SettableDraweeHierarchy) hierarchy; mSettableDraweeHierarchy.setControllerOverlay(mControllerOverlay); }}}Copy the code

The actual implementation of this mSettableDraweeHierarchy class is GenericDraweeHierarchy,

At this point, the binding process between DraweeController and DraweeHierarchy is complete.

1.4 Get the image from memory cache/disk cache/network and set it to the corresponding Drawable layer

👉 Sequence Figure 4.1 -> 4.14

The content of this section mainly performs the various Producer created above, obtains pictures from the memory cache/disk cache/network, and calls the corresponding Consumer consumption results. Finally, different Drawable Settings are put into the corresponding layers. We will talk about DraweeHierarchy and Producer in detail in the following sections. Let’s first look at how the top layer requests to images are finally set in SimpleDraweeView, as shown below:

public class GenericDraweeHierarchy implements SettableDraweeHierarchy {
    @Override
    public void setImage(Drawable drawable, float progress, boolean immediate) {
      drawable = WrappingUtils.maybeApplyLeafRounding(drawable, mRoundingParams, mResources);
      drawable.mutate();
      //mActualImageWrapper is the layer that actually loads the image, the image that SimpleDraweeView will eventually display.
      mActualImageWrapper.setDrawable(drawable);
      mFadeDrawable.beginBatchMode();
      fadeOutBranches();
      fadeInLayer(ACTUAL_IMAGE_INDEX);
      setProgress(progress);
      if(immediate) { mFadeDrawable.finishTransitionImmediately(); } mFadeDrawable.endBatchMode(); }}Copy the code

The mActualImageWrapper is the layer that actually loads the image, and the image that SimpleDraweeView will eventually display is set here.

In this way, a SimpleDraweeView image loading process is completed, facing such a long process, readers can not help wondering, as long as we master the overall process, we can divide and conquer, one by one.

Two DraweeHierarchy

Fresco’s image effects rely on Drawee, the Drawable hierarchy.

DraweeHierarchy is a Drawable hierarchy in Fresco that is layered on top of DraweeView to achieve various effects, such as: DraweeHierarchy is an interface. It also has a sub-interface SettableDraweeHierarchy. Their implementation class is GenericDraweeHierarchy.

The DraweeHierarchy interface and SettableDraweeHierarchy interface are as follows:

public interface DraweeHierarchy {
  // Get the top-level Drawable, which is the parent layer
  Drawable getTopLevelDrawable(a);
}

public interface SettableDraweeHierarchy extends DraweeHierarchy {
  // The DraweeHierarchy state is reset when called by DraweeController
  void reset(a);
   // Called by DraweeController, progress is used in progressive JPEG, immediate indicates whether the image is displayed immediately
  void setImage(Drawable drawable, float progress, boolean immediate);
   // Called by DraweeController to update the image loading progress [0, 1]. Progress is hidden when progress is 1 or immediate is true.
  void setProgress(float progress, boolean immediate);
   // The DraweeController is used to set the failure reason. DraweeHierarchy can display different failure pictures according to different reasons.
  void setFailure(Throwable throwable);
   // It is called by DraweeController to set the retry reason. DraweeHierarchy can display different retry pictures according to different reasons.
  void setRetry(Throwable throwable);
   // Called by DraweeController to set other Controller overlays
  void setControllerOverlay(Drawable drawable);
}
Copy the code

Understanding the general interface of DraweeHierarchy, we continue to parse DraweeHierarchy from the following perspectives:

  • Layer hierarchy construction
  • Layer creation process

2.1 Layer hierarchy construction

Fresco defines a number of drawables that directly or indirectly inherit them to perform different functions. Their layers are as follows:

o RootDrawable (top level drawable)
|
+--o FadeDrawable
  |
  +--o ScaleTypeDrawable (placeholder branch, optional)
  |  |
  |  +--o Drawable (placeholder image)
  |
  +--o ScaleTypeDrawable (actual image branch)
  |  |
  |  +--o ForwardingDrawable (actual image wrapper)
  |     |
  |     +--o Drawable (actual image)
  |
  +--o null (progress bar branch, optional)
  |
  +--o Drawable (retry image branch, optional)
  |
  +--o ScaleTypeDrawable (failure image branch, optional)
     |
     +--o Drawable (failure image)
Copy the code

There are a number of Fresco Drawable subclasses, which can be divided into three main categories by function:

Container classes Drawable

  • ArrayDrawable: Draw the Drawable in the same order as the LayerDrawable. The last member of the array will be at the top, but there are some differences between LayerDrawable and LayerDrawable: The drawing order is array order, but ArrayDrawable skips layers that do not need to be drawn for now. ② Dynamic layer addition is not supported.
  • FadeDrawable: Inherits ArrayDrawable. In addition to ArrayDrawable, it can also hide and show layers.

Container classes Drawable

  • ForwardingDrawable: Internally gives the patient a Drawable member variable that passes some basic operations and callbacks to the target Drawable. It is the base class for all container classes Drawable.
  • ScaleTypeDrawable: Inherits from ForwardingDrawable, encapsulating the scaling of proxy images.
  • SettableDrawable: a container derived from ForwardingDrawable that can be used to set content Drawable multiple times, used in the target image layer.
  • AutoRotateDrawable: A container that inherits from ForwardingDrawable and provides dynamic rotation of content.
  • OrientedDrawable: A container that inherits a ForwardingDrawable and draws the content Drawable at a particular Angle.
  • MatrixDrawable: a container derived from ForwardingDrawable that can apply a deformation matrix to content, which can only be assigned to the layer that displays the target image. You cannot use MatrixDrawable and ScaleTypeDrawable on the same layer!
  • RoundedCornersDrawable: A container that inherits from ForwardingDrawable and can trim the edges of content into rounded rectangles (currently not supported) or cover content with solid rounded rectangles.
  • GenericDraweeHierarchy. RootDrawable: inheritance in ForwardingDrawable, specifically for the top layer of the container.

The view class Drawable

  • ProgressBarDrawable: Responsible for drawing the progress bar.
  • RoundedBitmapDrawable: Trim its content into a rounded rectangle and draw it out. You can use a Bitmap as an object and return a BitmapDrawable.

In addition to these Drawable classes, there is also a Drawable interface, which is implemented by all drawables that perform matrix transformations and rounded corners. This interface is used to enable the child Drawable to draw its parent Drawable’s transformation matrix and rounded corners.

As follows:

public interface TransformAwareDrawable {
  // Sets the TransformCallback callback
  void setTransformCallback(TransformCallback transformCallback);
}

public interface TransformCallback {
  // Get all matrices of matrices applied to Drawable, stored in transform
  void getTransform(Matrix transform);
  // Get the root bounds of the Drawable, stored in the bounds.
  void getRootBounds(RectF bounds);
}
Copy the code

From the user’s point of view, the layers on SimpleDraweeView are divided into the following layers:

  • backgroundImage
  • PlaceholderImage =
  • actualImage
  • Progress bar (progressBarImage)
  • Retry the loaded image (retryImage)
  • failureImage
  • overlayImage

With this layer hierarchy understood, let’s move on to the layer creation process. 👇

2.2 Layer creation process

In the constructor of GenericDraweeView, we call its inflateHierarchy() method to build a GenericDraweeHierarchy object. GenericDraweeHierarchy is actually by GenericDraweeHierarchyBuild call build () method.

GenericDraweeHierarchy is the carrier responsible for loading each layer’s information. Let’s take a look at the implementation of its constructor, as follows:

public class GenericDraweeHierarchy implements SettableDraweeHierarchy {
    
      // As we said above, 7 layers.
      
      // Background layer
      private static final int BACKGROUND_IMAGE_INDEX = 0;
      // Placeholder layer
      private static final int PLACEHOLDER_IMAGE_INDEX = 1;
      // Load the image layer
      private static final int ACTUAL_IMAGE_INDEX = 2;
      / / the progress bar
      private static final int PROGRESS_BAR_IMAGE_INDEX = 3;
      // Retry the loaded image
      private static final int RETRY_IMAGE_INDEX = 4;
      // Failed image
      private static final int FAILURE_IMAGE_INDEX = 5;
      / / overlay chart
      private static final int OVERLAY_IMAGES_INDEX = 6;
    
      GenericDraweeHierarchy(GenericDraweeHierarchyBuilder builder) {
        mResources = builder.getResources();
        mRoundingParams = builder.getRoundingParams();
    
        // Actually load the Drawable of the image
        mActualImageWrapper = new ForwardingDrawable(mEmptyActualImageDrawable);
    
        intnumOverlays = (builder.getOverlays() ! =null)? builder.getOverlays().size() :1; numOverlays += (builder.getPressedStateOverlay() ! =null)?1 : 0;
    
        // Number of layers
        int numLayers = OVERLAY_IMAGES_INDEX + numOverlays;
    
        // array of layers
        Drawable[] layers = new Drawable[numLayers];
        //1. Create the background layer "Drawable".
        layers[BACKGROUND_IMAGE_INDEX] = buildBranch(builder.getBackground(), null);
        //2. Construct the placeholder layer Drawable.
        layers[PLACEHOLDER_IMAGE_INDEX] = buildBranch(
            builder.getPlaceholderImage(),
            builder.getPlaceholderImageScaleType());
        //3. Construct the loaded image layer "Drawable".
        layers[ACTUAL_IMAGE_INDEX] = buildActualImageBranch(
            mActualImageWrapper,
            builder.getActualImageScaleType(),
            builder.getActualImageFocusPoint(),
            builder.getActualImageColorFilter());
        //4. Create the progress bar layer "Drawable".
        layers[PROGRESS_BAR_IMAGE_INDEX] = buildBranch(
            builder.getProgressBarImage(),
            builder.getProgressBarImageScaleType());
        //5. Construct the reloaded image layer "Drawable".
        layers[RETRY_IMAGE_INDEX] = buildBranch(
            builder.getRetryImage(),
            builder.getRetryImageScaleType());
        //6. Failed to build the image layer Drawable.
        layers[FAILURE_IMAGE_INDEX] = buildBranch(
            builder.getFailureImage(),
            builder.getFailureImageScaleType());
        if (numOverlays > 0) {
          int index = 0;
          if(builder.getOverlays() ! =null) {
            for (Drawable overlay : builder.getOverlays()) {
              //7. Build the overlay layer Drawable.
              layers[OVERLAY_IMAGES_INDEX + index++] = buildBranch(overlay, null); }}else {
            index = 1; // reserve space for one overlay
          }
          if(builder.getPressedStateOverlay() ! =null) {
            layers[OVERLAY_IMAGES_INDEX + index] = buildBranch(builder.getPressedStateOverlay(), null); }}// fade drawable composed of layers
        mFadeDrawable = new FadeDrawable(layers);
        mFadeDrawable.setTransitionDuration(builder.getFadeDuration());
    
        // rounded corners drawable (optional)
        Drawable maybeRoundedDrawable =
            WrappingUtils.maybeWrapWithRoundedOverlayColor(mFadeDrawable, mRoundingParams);
    
        // top-level drawable
        mTopLevelDrawable = newRootDrawable(maybeRoundedDrawable); mTopLevelDrawable.mutate(); resetFade(); }}Copy the code

This method basically builds a Drawable object for each layer, as shown below:

  1. Create the background layer “Drawable”.
  2. Construct the placeholder layer “Drawable”.
  3. Build the loaded image layer “Drawable”.
  4. Construct the progress bar layer “Drawable”.
  5. Construct the reloaded image layer “Drawable”.
  6. Failed to build the image layer Drawable.
  7. Construct the overlay graph layer Drawable.

The built method is designed to have two methods

public class GenericDraweeHierarchy implements SettableDraweeHierarchy {
     @Nullable
     private Drawable buildActualImageBranch( Drawable drawable, @Nullable ScaleType scaleType, @Nullable PointF focusPoint, @Nullable ColorFilter colorFilter) {
       drawable.setColorFilter(colorFilter);
       drawable = WrappingUtils.maybeWrapWithScaleType(drawable, scaleType, focusPoint);
       return drawable;
     }
   
     /** Applies scale type and rounding (both if specified). */
     @Nullable
     private Drawable buildBranch(@Nullable Drawable drawable, @Nullable ScaleType scaleType) {
       RoundedBitmapDrawable or RoundedColorDrawable if Round, RoundedBitmapDrawable or RoundedColorDrawable is required.
       drawable = WrappingUtils.maybeApplyLeafRounding(drawable, mRoundingParams, mResources);
       // If you want to set ScaleType for Drawable, wrap it as a ScaleTypeDrawable.
       drawable = WrappingUtils.maybeWrapWithScaleType(drawable, scaleType);
       returndrawable; }}Copy the code

Whenever you build a Drawable, apply the appropriate scale type and rounded Angle, as shown below:

  • Set Round, RoundedBitmapDrawable, or RoundedColorDrawable for Drawable.
  • If you need to set a ScaleType for Drawable, wrap it as a ScaleTypeDrawable.

GenericDraweeHierarchy, the carrier of such a layer, is constructed. Subsequent operations in GenericDraweeHierarchy are completed by calling various Drawable methods inside the generator.

Three Producer and Consumer

As we said earlier, Producer is Fresco’s best model, doing all the dirty work. Look at the implementation.

public interface Producer<T> {
  // Start processing the task, execute the result to right Consumer to consume.
  void produceResults(Consumer<T> consumer, ProducerContext context);
}
Copy the code

Fresco implements multiple producers, which can be divided into the following categories:

Local data capture Class P Roducers, where producers are responsible for retrieving data from local producers.

  • LocalFetchProducer: The base class that implements the Producer interface and obtains all local data from Producer.

  • LocalAssetFetchProducer inherits from LocalFetchProducer, obtains the ImageRequest input stream and the object bytecode length through AssetManager, and converts it to EncodedImage.

  • LocalContentUriFetchProducer inheritance in LocalFetchProducer, if a Uri pointing to the contact, the contact to obtain the pictures; If it points to the photo in the album, some scaling will be performed according to whether ResizeOption is passed in (here it is not completely scaled according to ResizeOption). If neither of these conditions is met, the input stream is directly called by openInputStream(Uri Uri) of ContentResolver and converted to EncodedImage.

  • LocalFileFetchProducer inherits from LocalFetchProducer and directly obtains the input stream from the specified file, thus converting it to EncodedImage.

  • LocalResourceFetchProducer inheritance in LocalFetchProducer, through the Resources function openRawResources get the input stream, which translates into EncodedImage.

  • LocalExifThumbnailProducer no inheritance in LocalFetchProducer, can obtain the Exif image Producer;

  • LocalVideoThumbnailProducer no inheritance in LocalFetchProducer, can obtain the video thumbnail Producer.

Producers are responsible for getting data from the Internet.

  • NetworkFetchProducer: Achieves the Producer interface and obtains picture data from the network.

The caching data fetching class Producer is responsible for fetching data from the cache.

  • BitmapMemoryCacheGetProducer it is an Immutable Producer, subsequent Producer only for packing;
  • BitmapMemoryCacheProducer in the memory cache of a decoded to get the data; If not, the data will be obtained from nextProducer and cached when the data is obtained.
  • BitmapMemoryCacheKeyMultiplexProducer is a subclass of MultiplexProducer, nextProducer BitmapMemoryCacheProducer, Multiple Imagerequests with the same decoded memory cache key are “merged” so that if the cache hits, they all get the data;
  • PostprocessedBitmapMemoryCacheProducer in the memory cache of a decoded for PostProcessor processed images. Its nextProducer is all PostProcessorProducer, because if it doesn’t get cached by PostProcess, it needs to PostProcess the pictures it gets. ; If not, the data can be obtained from nextProducer.
  • EncodedMemoryCacheProducer without decoding search for data in the memory cache, if find it returns, use after the release of resources; If not, the data will be obtained from nextProducer and cached when the data is obtained.
  • EncodedCacheKeyMultiplexProducer is a subclass of MultiplexProducer, nextProducer EncodedMemoryCacheProducer, Multiple Imagerequests with the same undecoded memory cache key are “merged” so that if the cache hits, they all get the data;
  • DiskCacheProducer retrieves data from the file memory cache. If not, the data is retrieved from nextProducer and cached when it is retrieved

The functional Producer class, which is initialized with a nextProducer, processes the results of the nextProducer.

  • Multiproducer “merges” multiple Imagerequests with the same CacheKey so that they all get data from nextProducer;
  • ThreadHandoffProducer executes the produceResult method of nextProducer in a background thread (thread pool capacity 1).
  • SwallowResultProducer swallows the data obtained by nextProducer and passes null to the Consumer onNewResult.
  • ResizeAndRotateProducer transforms EncodedImage generated by nextProducer according to rotation and scaling properties of EXIF (if the object is not a JPEG image, it will not be transformed);
  • PostProcessorProducer modifies EncodedImage generated by nextProducer according to PostProcessor. See modified picture for PostProcessor.
  • DecodeProducer decodes EncodedImage generated by nextProducer. Decoding is performed in background threads. The number of thread pools can be set using setExecutorSupplier in ImagePipelineConfig. The default is the maximum number of processors available.
  • WebpTranscodeProducer If EncodedImage generated by nextProducer is in WebP format, it is decoded into EncodedImage that DecodeProducer can process. Decoding takes place in the descendant process.

So where are producers built? 🤔

In front of us said, build a DataSource, invoked ProducerSequenceFactory. GetDecodedImageProducerSequence (imageRequest); Methods for the specified ImageRequest build want Producer sequence, in fact, in the ProducerSequenceFactory besides getDecodedImageProducerSequence () method of thought, There are also several ways to get a sequence in other situations. Here’s a list of how a Producer’s sequence looks when getting pictures from the web.

As follows:

  1. PostprocessedBitmapMemoryCacheProducer, not have to, to look for in the Bitmap caching been PostProcess data.
  2. PostprocessorProducer postprocesses data sent by the lower Producer, which is not required.
  3. Read-only BitmapMemoryCacheGetProducer, must, make the Producer sequence.
  4. ThreadHandoffProducer must make the lower Producer work in a background process.
  5. BitmapMemoryCacheKeyMultiplexProducer, must, make more identical decoded memory cache key ImageRequest are to get the data from the same Producer.
  6. BitmapMemoryCacheProducer, must be from a decoded to get the data in the memory cache.
  7. DecodeProducer must decode the data produced by the lower Producer.
  8. ResizeAndRotateProducer is not required to transform the data generated by the lower Producer.
  9. EncodedCacheKeyMultiplexProducer, must, make more identical not decode the memory cache key ImageRequest are to get the data from the same Producer.
  10. EncodedMemoryCacheProducer, must be, and I have never been decoded to get the data in the memory cache.
  11. DiskCacheProducer must fetch data from the file cache.
  12. The WebpTranscodeProducer is not required to decode the Webp (if any) produced by the lower Producer.
  13. NetworkFetchProducer has to get data from the network.

We said that the results produced by Producer are consumed by consumers. How is that created? 🤔

The Producer processes the data and passes it downward, while the Consumer receives the results and passes it upward. Basically, the Producer wraps around the Consumer who receives the data. Let’s take a small example.

In the process analysis above, we create a DataSource is CloseableProducerToDataSourceAdapter said, in the end, CloseableProducerToDataSourceAdapter is the parent of the AbstractProducerToDataSourceAdapter, in its constructor invokes createConsumer () to create the first layer of Consumer, As follows:

public abstract class AbstractProducerToDataSourceAdapter<T> extends AbstractDataSource<T> {
    
     private Consumer<T> createConsumer(a) {
       return new BaseConsumer<T>() {
         @Override
         protected void onNewResultImpl(@Nullable T newResult, @Status int status) {
           AbstractProducerToDataSourceAdapter.this.onNewResultImpl(newResult, status);
         }
   
         @Override
         protected void onFailureImpl(Throwable throwable) {
           AbstractProducerToDataSourceAdapter.this.onFailureImpl(throwable);
         }
   
         @Override
         protected void onCancellationImpl(a) {
           AbstractProducerToDataSourceAdapter.this.onCancellationImpl();
         }
   
         @Override
         protected void onProgressUpdateImpl(float progress) {
           AbstractProducerToDataSourceAdapter.this.setProgress(progress); }}; }}Copy the code

As can be seen from the listed above sequence of Producer, Producer of the first layer is PostprocessedBitmapMemoryCacheProducer, in its produceResults () method, will be passed down to the above Consumer packaging, As follows:

public class PostprocessedBitmapMemoryCacheProducer
    implements Producer<CloseableReference<CloseableImage>> {
    
     @Override
     public void produceResults(
         final Consumer<CloseableReference<CloseableImage>> consumer,
         final ProducerContext producerContext) {
         / /...
         final boolean isRepeatedProcessor = postprocessor instanceof RepeatedPostprocessor;
         Consumer<CloseableReference<CloseableImage>> cachedConsumer = new CachedPostprocessorConsumer(
             consumer,
             cacheKey,
             isRepeatedProcessor,
             mMemoryCache);
         listener.onProducerFinishWithSuccess(
             requestId,
             getProducerName(),
             listener.requiresExtraMap(requestId) ? ImmutableMap.of(VALUE_FOUND, "false") : null);
         mInputProducer.produceResults(cachedConsumer, producerContext);
         / /...}}Copy the code

When their own produceResults PostprocessedBitmapMemoryCacheProducer call () deal with their own task, will continue to call the Producer of the next layer, when all the Producer to finish their work, The result is then returned from the bottom layer to the top Consumer callback, which ultimately returns the result to the caller.

The producers in Fresco are ranked in an order, executing each Producer and moving on to the next.

That’s the whole Producer/Consumer structure in Fresco.

3 cache Mechanism

Fresco has three levels of cache, two levels of memory cache, and one level of disk cache, as shown below:

👉 Click on the image for a larger version

  • Unencoded image memory cache
  • Encoded image memory cache
  • Disk cache

Disk caching because it involves reading and writing files is more complex than memory caching, disk caching can be divided into three layers from the bottom up:

  • BufferedDiskCache layer: Implemented by BufferedDiskCache, provides buffering.
  • File caching layer: Implemented by DiskStroageCache, provides the actual caching function.
  • File storage layer: Implemented by DefaultDiskStorage, it provides read and write functions for disk files.

Let’s take a look at Fresco’s cache key design. Fresco designs an interface for cache keys, as follows:

public interface CacheKey {
  String toString(a);
  boolean equals(Object o);
  int hashCode(a);
  // whether it is built from a Uri
  boolean containsUri(Uri uri);
  // Get the URL string
  String getUriString(a);
}
Copy the code

CacheKey has two implementation classes:

  • BitmapMemoryCacheKey is used for a decoded memory cache key that uses hashCode as a unique identifier for key parameters such as the Uri string, zoom size, decoding parameters, and PostProcessor.
  • SimpleCacheKey A common implementation of a cache key that uses the hashCode of the string passed in as a unique identifier, so you need to ensure that the same string is passed in for the same key.

Ok, let’s move on to the implementation of memory caching and disk caching.

3.1 Memory Cache

As we mentioned earlier, there are two levels of memory caching:

  • Undecoded image memory cache: Real cached objects are described by EncodedImage.
  • Decoded image memory cache: The actual cached object is described by BitmapMemoryCache.

The difference between them is that the data format of the cache is different. The unencoded image memory cache uses CloseableReference and the encoded image memory cache uses CloseableReference. The difference between them is that the resource is measured and released in different ways. They use vauledescriptors to describe the data size of different resources, and different resourcereleasers to release resources.

The internal data structure used is CountingLruMap, we previously in article 07Android open source framework source appreciation: Both LruCache and DiskLruCache use LinkedHashMap. Instead of using LinkedHashMap directly, Fresco encapsulates it. This is CountingLruMap, which has a two-way linked list inside. When searching, you can start the query from the earliest inserted unit, so that you can quickly delete the earliest inserted data and improve efficiency.

Let’s look at how memory caching is implemented. The implementation of memory caching comes from a common interface, as follows:

public interface MemoryCache<K.V> {
  // Cache the specified key-value pair. This method returns a new copy of the cache used to code the original reference, but closes the reference if it is not needed
  CloseableReference<V> cache(K key, CloseableReference<V> value);
  // Get the cache
  CloseableReference<V> get(K key);
  // Remove the cache with the specified key
  public int removeAll(Predicate<K> predicate);
  // Check whether the cache corresponding to the key is included
  public boolean contains(Predicate<K> predicate);
}
Copy the code

MemoryTrimmableRegistry Implements the MemoryTrimmableRegistry interface and notifies the user of any memory changes, as shown in the following figure:

public interface MemoryTrimmable {
  // Memory changes
  void trim(MemoryTrimType trimType);
}

Copy the code

Let’s take a look at which classes implement this caching interface directly or indirectly.

  • CountingMemoryCache. It implements MemoryCache and MemoryTrimmable interfaces. This Entry is internally maintained to encapsulate the cache object. The Entry object not only records the cache key and cache value, but also records the number of references (clientCount) of the object. And whether the cache is tracked (isOrphan).
  • InstrumentedMemoryCache: This instrument implements the MemoryCache interface, but does not implement it directly. It acts as a Wrapper class, wrapping CountingMemoryCache. A MemoryCacheTracker was added to provide a callback function for callers to implement custom functionality in the event of a cache miss.

An Entry object is used inside CountingMemoryCache to describe the cache pair, which contains the following information:

  static class Entry<K.V> {
    / / the cache key
    public final K key;
    // Cache objects
    public final CloseableReference<V> valueRef;
    // The number of clients that reference the value.
    // Cache reference count
    public int clientCount;
    // Whether the Entry object is tracked by the cache it describes
    public boolean isOrphan;
    // Cache status listener
    @Nullable public final EntryStateObserver<K> observer;
}
Copy the code

👉 Note: Cache objects can be released only when the number of references (clientCount) is 0 and the cache is not tracked (isOrphan = true).

Let’s take a look at how CountingMemoryCache inserts, retrives, and deletes caches.

Inserted into the cache

First, we need to understand that the operation of the cache involves two collections:

  // The cache set to be removed is not being used outside
  @VisibleForTesting
  final CountingLruMap<K, Entry<K, V>> mExclusiveEntries;

  // A collection of all caches, including those to be removed
  @GuardedBy("this")
  @VisibleForTesting
  final CountingLruMap<K, Entry<K, V>> mCachedEntries;
Copy the code

Let’s move on to the implementation of the insert cache.

public class CountingMemoryCache<K.V> implements MemoryCache<K.V>, MemoryTrimmable {
    
      public CloseableReference<V> cache(
          final K key,
          final CloseableReference<V> valueRef,
          final EntryStateObserver<K> observer) {
        Preconditions.checkNotNull(key);
        Preconditions.checkNotNull(valueRef);
    
        1. Check whether the cache parameters need to be updated.
        maybeUpdateCacheParams();
    
        Entry<K, V> oldExclusive;
        CloseableReference<V> oldRefToClose = null;
        CloseableReference<V> clientRef = null;
        synchronized (this) {
          //2. Search the cache for the object to be inserted, remove it from the cache to be removed if it exists, and call its close() method
          // The cache object is released when its number of references reaches zero.
          oldExclusive = mExclusiveEntries.remove(key);
          Entry<K, V> oldEntry = mCachedEntries.remove(key);
          if(oldEntry ! =null) {
            makeOrphan(oldEntry);
            oldRefToClose = referenceToClose(oldEntry);
          }
          //3. Check whether the cache objects are displayed to the maximum or the cache pool is full. If both are not, insert a new cache object.
          if (canCacheNewValue(valueRef.get())) {
            Entry<K, V> newEntry = Entry.of(key, valueRef, observer);
            mCachedEntries.put(key, newEntry);
            //4. Wrap the inserted object as a CloseableReference, rewrap the object mainly for reset
            // Go to ResourceReleaser, which reduces the Entry clientCount when releasing the resource and will cache the object
            // Add to mExclusiveEntries, mExclusiveEntries are the cache that has been used (waiting to be released),
            // If the cache object can be released, the cache object is released directly.
            clientRef = newClientReference(newEntry);
          }
        }
        CloseableReference.closeSafely(oldRefToClose);
        maybeNotifyExclusiveEntryRemoval(oldExclusive);
    
        //5. Determine whether the resource needs to be released. If the EvictEntries maximum capacity or the cache pool is full, remove the object that EvictEntries first inserts.
        maybeEvictEntries();
        returnclientRef; }}Copy the code

Insert cache does several things:

  1. Check whether cache parameters need to be updated.
  2. It looks for the object to be inserted in the cache, removes it from the cache to be removed if it exists, and calls its close() method to release the object when the number of references to the cache object reaches zero.
  3. Check whether the cache object reaches the maximum display or the cache pool is full. If both are not, insert a new cache object.
  4. Wrap the inserted object as CloseableReference. The main purpose of rewrapping the object is to reset ResourceRelr, which reduces the clientCount of the Entry when releasing the resource and adds the cache object to mExclusiveEntries. MExclusiveEntries hold used caches (waiting to be released). If the cache can be freed, release the cache.
  5. Determine whether the resource needs to be released. When the EvictEntries maximum capacity is exceeded or the cache pool is full, remove the object that EvictEntries first inserts.

Access to the cache

public class CountingMemoryCache<K.V> implements MemoryCache<K.V>, MemoryTrimmable {
    
      @Nullable
      public CloseableReference<V> get(final K key) {
        Preconditions.checkNotNull(key);
        Entry<K, V> oldExclusive;
        CloseableReference<V> clientRef = null;
        synchronized (this) {
          //1. Query the cache, indicating that the cache may be used, and try to remove it from the cache to be removed.
          oldExclusive = mExclusiveEntries.remove(key);
          //2. Query the cache from the cache collection.
          Entry<K, V> entry = mCachedEntries.get(key);
          if(entry ! =null) {
            //3. If the cache is found, wrap the cache object as a CloseableReference. Rewrap the object mainly for reset
           // Go to ResourceReleaser, which reduces the Entry clientCount when releasing the resource and will cache the object
            // Add to mExclusiveEntries, mExclusiveEntries are the cache that has been used (waiting to be released),
            // If the cache object can be released, the cache object is released directly.clientRef = newClientReference(entry); }}//4. Determine whether you need to notify the set to be deleted that the element has been removed.
        maybeNotifyExclusiveEntryRemoval(oldExclusive);
        Check whether cache parameters need to be updated.
        maybeUpdateCacheParams();
        //6. Determine whether the resource needs to be released. If the EvictEntries maximum capacity or the cache pool is full, remove the object that EvictEntries first inserts.
        maybeEvictEntries();
        returnclientRef; }}Copy the code

The following operations are performed to obtain the cache:

  1. Query the cache, indicating that the cache may be used, and try to remove it from the cache collection to be removed.
  2. Query the cache from the cache collection.
  3. If the cache is found, the cache object is packaged as a CloseableReference. The main purpose of repackaging the object is to reset the ResourceReleaser, which reduces the clientCount of entries when releasing resources. Add the cache object to mExclusiveEntries, which hold the used cache (waiting to be freed). If the cache object can be freed, the cache object can be freed. Determine whether you need to notify the collection that the element has been removed.
  4. Determine whether cache parameters need to be updated.
  5. Determine whether the resource needs to be released. When the EvictEntries maximum capacity is exceeded or the cache pool is full, remove the object that EvictEntries first inserts.

Remove the cache

To remove the cache, call the collection’s removeAll() method to removeAll elements, as shown below:

public class CountingMemoryCache<K.V> implements MemoryCache<K.V>, MemoryTrimmable {
    
      public int removeAll(Predicate<K> predicate) {
        ArrayList<Entry<K, V>> oldExclusives;
        ArrayList<Entry<K, V>> oldEntries;
        synchronized (this) {
          oldExclusives = mExclusiveEntries.removeAll(predicate);
          oldEntries = mCachedEntries.removeAll(predicate);
          makeOrphans(oldEntries);
        }
        maybeClose(oldEntries);
        maybeNotifyExclusiveEntryRemoval(oldExclusives);
        maybeUpdateCacheParams();
        maybeEvictEntries();
        returnoldEntries.size(); }}Copy the code

This method is simple, but we focus on a method that occurs many times: maybeEvictEntries(), which adjusts the total cache size to the maximum number and size, as shown below:

public class CountingMemoryCache<K.V> implements MemoryCache<K.V>, MemoryTrimmable {
    
      private void maybeEvictEntries(a) {
        ArrayList<Entry<K, V>> oldEntries;
        synchronized (this) {
          int maxCount = Math.min(
              // The maximum number of caches held by the collection to be removed
              mMemoryCacheParams.maxEvictionQueueEntries,
              // Maximum number of caches held by cache set - the number of caches currently in use
              mMemoryCacheParams.maxCacheEntries - getInUseCount());
          int maxSize = Math.min(
              // The maximum cache capacity of the collection to be removed
              mMemoryCacheParams.maxEvictionQueueSize,
              // Maximum cache capacity held by cache collection - the current cache capacity in use
              mMemoryCacheParams.maxCacheSize - getInUseSizeInBytes());
          //1. Continue to remove the head of the queue from mExclusiveEntries according to maxCount and maxSize until the cache limit is satisfied.
          oldEntries = trimExclusivelyOwnedEntries(maxCount, maxSize);
          //2. Set isOrphan of cache entries to true, indicating that the Entry object is no longer tracked and waiting to be deleted.
          makeOrphans(oldEntries);
        }
        //3. Disable the cache.
        maybeClose(oldEntries);
        //4. The notification cache is disabled.maybeNotifyExclusiveEntryRemoval(oldEntries); }}Copy the code

The entire capacity adjustment process is based on the current number and capacity of caches until the maximum number and capacity of caches are met, as shown below:

  1. According to maxCount and maxSize, continue to remove the head of the queue from mExclusiveEntries until the cache limit is satisfied.
  2. Setting the isOrphan of the cache Entry to true indicates that the Entry object is no longer tracked, waiting to be deleted.
  3. Turn off the cache.
  4. Notification cache is disabled.

That’s it for memory caching, but let’s move on to the implementation of disk caching. 👇

3.2 Disk Caching

As we have already said, disk caching is also divided into three layers, as shown in the following figure:

👉 Click on the image for a larger version

Disk caching because it involves reading and writing files is more complex than memory caching, disk caching can be divided into three layers from the bottom up:

  • BufferedDiskCache layer: Implemented by BufferedDiskCache, provides buffering.
  • File caching layer: Implemented by DiskStroageCache, provides the actual caching function.
  • File storage layer: Implemented by DefaultDiskStorage, it provides read and write functions for disk files.

Let’s look at the relevant interfaces.

The disk cache interface is FileCache, as shown below:

public interface FileCache extends DiskTrimmable {
  // Whether disk caching can be performed, mainly depends on whether local storage exists and whether it can be read and written.
  boolean isEnabled(a);
  // Returns the cached binary resource
  BinaryResource getResource(CacheKey key);
  // Whether to include the cache key, call asynchronously.
  boolean hasKeySync(CacheKey key);
  // Whether to include the cache key, synchronous call.
  boolean hasKey(CacheKey key);
  boolean probe(CacheKey key);
  // Insert the cache
  BinaryResource insert(CacheKey key, WriterCallback writer) throws IOException;
  // Remove the cache
  void remove(CacheKey key);
  // Get the total cache size
  long getSize(a);
   // Get the number of caches
  long getCount(a);
  // Clear expired caches
  long clearOldEntries(long cacheExpirationMs);
  // Clear all caches
  void clearAll(a);
  // Obtain disk dump information
  DiskStorage.DiskDumpInfo getDumpInfo(a) throws IOException;
}
Copy the code

You can see that the FileCahce interface inherits from DisTrimmable, which is an interface used to listen for disk capacity changes, as follows:

public interface DiskTrimmable {
  // Call back when the disk space is low.
  void trimToMinimum(a);
  // When the disk space is no longer available
  void trimToNothing(a);
}

Copy the code

In addition to the DiskStorageCache cache interface, Fresco also defines the DiskStorage interface to encapsulate the read and write logic of the FILE I/O, as shown below:

public interface DiskStorage {

  class DiskDumpInfoEntry {
    public final String path;
    public final String type;
    public final float size;
    public final String firstBits;
    protected DiskDumpInfoEntry(String path, String type, float size, String firstBits) {
      this.path = path;
      this.type = type;
      this.size = size;
      this.firstBits = firstBits; }}class DiskDumpInfo {
    public List<DiskDumpInfoEntry> entries;
    public Map<String, Integer> typeCounts;
    public DiskDumpInfo(a) {
      entries = new ArrayList<>();
      typeCounts = newHashMap<>(); }}// Whether the file storage is available
  boolean isEnabled(a);
  // Whether to include external storage
  boolean isExternal(a);
  // Get the file to which the file descriptor points
  BinaryResource getResource(String resourceId, Object debugInfo) throws IOException;
  // Check whether the file indicated by the file descriptor is included
  boolean contains(String resourceId, Object debugInfo) throws IOException;
  // Check whether the file corresponding to the resourceId exists, and if so, update the last read timestamp.
  boolean touch(String resourceId, Object debugInfo) throws IOException;
  void purgeUnexpectedResources(a);
  / / insert
  Inserter insert(String resourceId, Object debugInfo) throws IOException;
  // Get all the entries in the disk cache.
  Collection<Entry> getEntries(a) throws IOException;
  // Remove the specified cache Entry.
  long remove(Entry entry) throws IOException;
  // Remove the disk cache file based on the resourceId
  long remove(String resourceId) throws IOException;
  // Clear all cached files
  void clearAll(a) throws IOException;

  DiskDumpInfo getDumpInfo(a) throws IOException;

  // Get the storage name
  String getStorageName(a);

  interface Entry {
    //ID
    String getId(a);
    / / timestamp
    long getTimestamp(a);
    / / size
    long getSize(a);
    //Fresco uses a BinaryResource object to describe the disk cache object, from which you can get the input stream of a file, bytecode, and so on.
    BinaryResource getResource(a);
  }

  interface Inserter {
    // Write data
    void writeData(WriterCallback callback, Object debugInfo) throws IOException;
    // Commit the data to be written
    BinaryResource commit(Object debugInfo) throws IOException;
    // Cancel the insertion operation
    boolean cleanUp(a); }}Copy the code

Now that we understand the functionality of the main interfaces, let’s look at the main implementation classes:

  • DiskStroageCache: implements the FileCache interface and the DiskTrimmable interface are the main implementation classes for caching.
  • DefaultDiskStorage: Implements the DiskStorage interface and encapsulates the read and write logic of disk I/O.
  • BufferedDiskCache: Provides Buffer function based on DiskStroageCache.

BufferedDiskCache provides three main functions:

  • StagingArea provides write buffer, where all data to be written will be stored before the write command is issued until the final write. When searching the cache, the data will be searched in this area first, and if hit, it will be returned directly.
  • WriteToDiskCache provides a way to write data. WriteToDiskCache provides a way for WriterCallback to write EncodedImage into an input stream.
  • Get and PUT are run in background threads (except for get lookup in the buffer area), each of which is a thread pool of capacity 2.

Let’s look at their implementation details.

The DiskStorage defines an interface Entry to describe the information of the disk cache object. The BinaryResource interface is the real holding of the cache object. Its implementation class is FileBinaryResource, which defines some operations on File. It can be used to get the input stream and bytecode of the file, etc.

In addition, Fresco defines a unique descriptor for each file, which is derived from the SHA-1 hash code of the string exported by CacheKey’s toString() method, which is then Base64 encrypted.

Let’s look at the implementation of insert, find, and delete for disk caching.

Inserted into the cache

public class DiskStorageCache implements FileCache.DiskTrimmable {
    
   @Override
     public BinaryResource insert(CacheKey key, WriterCallback callback) throws IOException {
       //1. Write the disk cache to the cache file first, which provides concurrent write cache speed.
       SettableCacheEvent cacheEvent = SettableCacheEvent.obtain()
           .setCacheKey(key);
       mCacheEventListener.onWriteAttempt(cacheEvent);
       String resourceId;
       synchronized (mLock) {
         //2. Obtain the cached resoucesId.
         resourceId = CacheKeyUtil.getFirstResourceId(key);
       }
       cacheEvent.setResourceId(resourceId);
       try {
         //3. Create the file to insert (synchronous operation). Here we build the Inserter object, which encapsulates the specific write process.
         DiskStorage.Inserter inserter = startInsert(resourceId, key);
         try {
           inserter.writeData(callback, key);
           //4. Commit the newly created cache file to the cache.
           BinaryResource resource = endInsert(inserter, key, resourceId);
           cacheEvent.setItemSize(resource.size())
               .setCacheSize(mCacheStats.getSize());
           mCacheEventListener.onWriteSuccess(cacheEvent);
           return resource;
         } finally {
           if(! inserter.cleanUp()) { FLog.e(TAG,"Failed to delete temp file"); }}}catch (IOException ioe) {
         / /... Exception handling
       } finally{ cacheEvent.recycle(); }}}Copy the code

The entire process for inserting the cache is as follows:

  1. The disk cache is written to the cache file first, which provides concurrent speed for the write cache.
  2. Get the resoucesId of the cache.
  3. Create the file to insert (synchronous operation), where you build the Inserter object, which encapsulates the specific write process.
  4. Commit the newly created cache file to the cache.

Let’s focus on the two methods startInsert() and endInsert().

public class DiskStorageCache implements FileCache.DiskTrimmable {
    
      // Create a temporary file with the suffix.tmp
      private DiskStorage.Inserter startInsert(
          final String resourceId,
          final CacheKey key)
          throws IOException {
        maybeEvictFilesInCacheDir();
        Call the insert() method of DefaultDiskStorage to create a temporary file
        return mStorage.insert(resourceId, key);
      }

      // If the cache file is already in the cache, how to delete the original file
      private BinaryResource endInsert(
          final DiskStorage.Inserter inserter,
          final CacheKey key,
          String resourceId) throws IOException {
        synchronized (mLock) {
          BinaryResource resource = inserter.commit(key);
          // Add a resourceId to the point resourceId set. DiskStorageCache maintains only this set
          // to record cache
          mResourceIndex.add(resourceId);
          mCacheStats.increment(resource.size(), 1);
          returnresource; }}}Copy the code

DiskStorageCache maintains only one Set, Set mResourceIndex, to record the cache Resource ID, and DefaultDiskStorage manages the cache on disk. The body provides the index function for DiskStorageCache.

Let’s look at the implementation of lookup caching.

Find the cache

Lookup the cache BinaryResource based on the CacheKey, updating its LRU access timestamp if the cache exists, or returning null if the cache does not exist.

public class DiskStorageCache implements FileCache.DiskTrimmable {
    
     @Override
     public BinaryResource getResource(final CacheKey key) {
       String resourceId = null;
       SettableCacheEvent cacheEvent = SettableCacheEvent.obtain()
           .setCacheKey(key);
       try {
         synchronized (mLock) {
           BinaryResource resource = null;
           //1. Get the cached Resourceids, here a list, because there may be MultiCacheKey, which wraps multiple CacheKeys.
           List<String> resourceIds = CacheKeyUtil.getResourceIds(key);
           for (int i = 0; i < resourceIds.size(); i++) {
             resourceId = resourceIds.get(i);
             cacheEvent.setResourceId(resourceId);
             //2. Obtain the BinaryResource corresponding to ResourceId.
             resource = mStorage.getResource(resourceId, key);
             if(resource ! =null) {
               break; }}if (resource == null) {
             //3. If the cache does not hit, execute the onMiss() callback and remove the resourceId from mResourceIndex.
             mCacheEventListener.onMiss(cacheEvent);
             mResourceIndex.remove(resourceId);
           } else {
             //4. If the cache hits, execute the onHit() callback and add resourceId to mResourceIndex.
             mCacheEventListener.onHit(cacheEvent);
             mResourceIndex.add(resourceId);
           }
           returnresource; }}catch (IOException ioe) {
         / /... Exception handling
         return null;
       } finally{ cacheEvent.recycle(); }}}Copy the code

The entire search process is as follows:

  1. Get the cached ResourceIDS, here a list, because there may be MultiCacheKey, which wraps multiple CacheKeys.
  2. Gets the BinaryResource corresponding to the ResourceId.
  3. If the cache does not hit, the onMiss() callback is executed and the resourceId is removed from mResourceIndex.
  4. The onHit() callback is executed and the resourceId is added to mResourceIndex. mCacheEventListener.onHit(cacheEvent);

The getReSource() method of DefaultDiskStorage is called to query the path of the cache file and build a BinaryResource object.

The path for Fresco to save the cache file locally is as follows:

parentPath + File.separator + resourceId + type;
Copy the code

ParentPath is the root directory, and there are two types of types:

  • private static final String CONTENT_FILE_EXTENSION = “.cnt”;
  • private static final String TEMP_FILE_EXTENSION = “.tmp”;

That’s the logic for querying the cache. Let’s look at the logic for deleting the cache.

Delete the cache

public class DiskStorageCache implements FileCache.DiskTrimmable {
    
      @Override
      public void remove(CacheKey key) {
        synchronized (mLock) {
          try {
            String resourceId = null;
            // Get the Resoucesid, remove the cache according to the resouceId, and remove itself from mResourceIndex.
            List<String> resourceIds = CacheKeyUtil.getResourceIds(key);
            for (int i = 0; i < resourceIds.size(); i++) { resourceId = resourceIds.get(i); mStorage.remove(resourceId); mResourceIndex.remove(resourceId); }}catch (IOException e) {
             / /... Remove the handle}}}}Copy the code

The logic for removing the cache is also simple, get the Resoucesid, remove the cache based on the resouceId, and remove yourself from the mResourceIndex.

The disk cache also adjusts its own cache size to meet the maximum cache size limit, so let’s take a look.

If the cache of a Fresco disk is overloaded, the disk will be cleared up to 90% of the cache capacity. The clearing process is as follows:

public class DiskStorageCache implements FileCache.DiskTrimmable {
    
      @GuardedBy("mLock")
      private void evictAboveSize(
          long desiredSize,
          CacheEventListener.EvictionReason reason) throws IOException {
        Collection<DiskStorage.Entry> entries;
        try {
          //1. Obtain a collection of entries of all files in the cache directory, in order of the most recently accessed time, with the most recently accessed entries followed.
          entries = getSortedEntries(mStorage.getEntries());
        } catch (IOException ioe) {
          / /... Catch exceptions
        }
    
        // The amount of data to delete
        long cacheSizeBeforeClearance = mCacheStats.getSize();
        long deleteSize = cacheSizeBeforeClearance - desiredSize;
        // Record the number of deleted data
        int itemCount = 0;
        // Record the size of the deleted data
        long sumItemSizes = 0L;
        //2. Loop over, removing elements from the header until the remaining capacity reaches the desiredSize position.
        for (DiskStorage.Entry entry: entries) {
          if (sumItemSizes > (deleteSize)) {
            break;
          }
          long deletedSize = mStorage.remove(entry);
          mResourceIndex.remove(entry.getId());
          if (deletedSize > 0) { itemCount++; sumItemSizes += deletedSize; SettableCacheEvent cacheEvent = SettableCacheEvent.obtain() .setResourceId(entry.getId()) .setEvictionReason(reason) .setItemSize(deletedSize) .setCacheSize(cacheSizeBeforeClearance - sumItemSizes) .setCacheLimit(desiredSize); mCacheEventListener.onEviction(cacheEvent); cacheEvent.recycle(); }}//3. Update the capacity and delete unnecessary temporary files.mCacheStats.increment(-sumItemSizes, -itemCount); mStorage.purgeUnexpectedResources(); }}Copy the code

The whole cleaning process can be divided into the following steps:

  1. Gets a collection of entries for all files in the cache directory, in order of the most recently accessed, followed by the most recently accessed Entry.
  2. The loop removes elements from the head until the remaining capacity reaches the desiredSize position.
  3. Update capacity and delete temporary files that are not needed.

This is the source code analysis of Fresco. I would like to talk about Fresco memory management, but it is related to Java Heap and Android anonymous shared memory, so wait for the future analysis of the Android Memory Management Framework.