Fresco is the framework for loading images in our project. I’m not responsible for the Fresco framework, but I’m responsible for loading and viewing images in the group, so knowing the source code of Fresco will help me in my future work, as well as learning about it.

There is too much source code for Fresco to cover in a single article, so I’ll cover it in detail as a series. This series of articles will try to describe Fresco as accurately as possible by referring to the source code for Fresco. If you have any mistakes, please point them out.

Fresco is a powerful image-loading component. After using it, you don’t need to worry about loading and displaying pictures! Supports Android 2.3 and later versions. To learn more about Fresco usage, visit the Fresco usage documentation.

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.
  • .

The composition of Fresco is fairly clear, as shown below:

      

These two graphs are actually from different articles, but I think the layering is actually basically the same. Just a general, a specific comparison, put the two together, it helps you to understand the implementation details. Of course, in addition to UI and loading display part, there are Gif, dynamic pictures and other content, as well as the corresponding picture decoding and coding logic. I’m not going to go into this section because it’s an important part of the source code, but it requires professional knowledge, and it involves C++ code.

The following code is used to explain the functions of the above modules and their working principles.

DraweeView

It inherits from ImageView and is used to display Fresco images during various stages of loading, such as loading a placeholder image and switching to a target image when the image is successfully loaded. However, the class may not inherit ImageView in the future, so it does not support setImageXxx, setScaleType, and other similar methods of ImageView. The only current interaction between DraweeView and ImageView is that it uses the ImageView to display Drawable:

//DraweeView.setController() public void setController(@Nullable DraweeController draweeController) { mDraweeHolder.setController(draweeController); super.setImageDrawable(mDraweeHolder.getTopLevelDrawable()); / / is super ImageView} / / DraweeHolder getTopLevelDrawable () public @ Nullable Drawable getTopLevelDrawable () {return mHierarchy == null ? null : mHierarchy.getTopLevelDrawable(); // mHierarchy is DraweeHierachy,}Copy the code

Draweeview.setcontroller () is called when Fresco loads the image. Fresco uses ImageView to display hierachy’s TopLevelDrawable. The above code leads to two other key classes in the UI layer: DraweeHolder and DraweeHierachy.

DraweeHierachy

It is the implementer of the Fresco image display. Its output is a Drawable, which is drawn by the DraweeView (as described above). It has multiple Drawable inside, and the Drawable currently displayed in DraweeView is called TopLevelDrawable. The TopLevelDrawable is different in different image loading stages (e.g. placeholder during loading and target image after loading). It implements the Drawable logic.

It is held directly by DraweeController, so switching between images is performed directly by DraweeController.

DraweeHolder

You can think of it as the glue between the DraweeView, DraweeHierachy, and DraweeController classes, DraweeView is not directly in contact with DraweeController and DraweeHierachy, all operations are passed through it. In this way, the subsequent change of DraweeView’s parent class to View does not affect other classes. The DraweeView, as a View, is click-aware and life-cycle aware, and controls the operations of the other two classes through the DraweeHolder.

Would you pull out a class like DraweeHolder? In fact, this is also a reference for our daily development. We can strictly control the relationship between each class and introduce some intermediate classes to reduce the coupling degree of the relationship between classes, which is convenient for future iterations.

The specific reference relationship is shown below:

Its main functions are: Receive image loading request of DraweeView, control ProducerSequence to initiate image loading and processing process, monitor events (failure, completion, etc.) in the loading process of ProducerSequence. And update the latest Drawable to DraweeHierachy.

The construction logic of DraweeController

In the Fresco is obtained through PipelineDraweeControllerBuilderSupplier DraweeController. Fresco calls the following code when it initializes:

// Fresco.java
private static void initializeDrawee(Context context, @Nullable DraweeConfig draweeConfig) {
    sDraweeControllerBuilderSupplier = new PipelineDraweeControllerBuilderSupplier(context, draweeConfig);
    SimpleDraweeView.initialize(sDraweeControllerBuilderSupplier);
}
Copy the code

SDraweeControllerBuilderSupplier is static variables, that is the only original one more time. All DraweeController is by calling sDraweecontrollerbuildersupplier. The get ().

private void init(Context context, @Nullable AttributeSet attrs) { try { if (FrescoSystrace.isTracing()) { FrescoSystrace.beginSection("SimpleDraweeView#init"); } if (isInEditMode()) { getTopLevelDrawable().setVisible(true, false); getTopLevelDrawable().invalidateSelf(); } else { Preconditions.checkNotNull( sDraweecontrollerbuildersupplier, "SimpleDraweeView was not initialized!" ); mControllerBuilder = sDraweecontrollerbuildersupplier.get(); // a call creates a new instance} //...... Omit other code}Copy the code

Fresco provides a DraweeController for each image load. The same DraweeController can be used for multiple images loaded from a DraweeView:

SimpleDraweeView.java public void setImageURI(Uri uri, @Nullable Object callerContext) { DraweeController controller = mControllerBuilder .setCallerContext(callerContext) .seturi (uri) // Set the new image loading path.setoldController (getController()) // reuse controller.build (); setController(controller); }Copy the code

So in general: one DraweeView for one DraweeController.

Initiate image loading via DataSource

As mentioned above, DraweeController holds DraweeHierachy directly, so it can easily update ProducerSequence data changes to DraweeHierachy (the specific code is not shown for now). How does it control the ProducerSequence to load the image? The DraweeController is not directly associated with the ProducerSequence. For image loading, it directly contacts the DataSource, and the DataSource controls the ProducerSequence to initiate image loading and processing. The DraweeController uses DataSource to control the ProducerSequence to load and process images.

// AbstractDraweeController.java protected void submitRequest() { mDataSource = getDataSource(); Final DataSubscriber<T> DataSubscriber = new BaseDataSubscriber<T>() OnNewResultImpl (DataSource<T> DataSource) { }... }; . mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor); / / mUiThreadImmediateExecutor refers to dataSubscriber callback methods running thread, this is the main thread}Copy the code

So what is a DataSource? GetDataSource () will eventually call:

// PipelineDraweeControllerBuilder 
 protected DataSource<CloseableReference<CloseableImage>> getDataSourceForRequest(
      DraweeController controller,
      String controllerId,
      ImageRequest imageRequest,
      Object callerContext,
      AbstractDraweeControllerBuilder.CacheLevel cacheLevel) {
    return mImagePipeline.fetchDecodedImage(
        imageRequest,
        callerContext,
        convertCacheLevelToRequestLevel(cacheLevel),
        getRequestListener(controller),
        controllerId);
  }
Copy the code

 

// CloseableProducerToDataSourceAdapter<T>
Copy the code
public static <T> DataSource<CloseableReference<T>> create( Producer<CloseableReference<T>> producer, SettableProducerContext settableProducerContext, RequestListener2 listener) { CloseableProducerToDataSourceAdapter<T> result = new CloseableProducerToDataSourceAdapter<T>(producer, settableProducerContext, listener); return result; }Copy the code

So DraweeController eventually get the DataSource is CloseableProducerToDataSourceAdapter. The class constructor calls producer.produceresults (…) to start the image-loading process. This method is the starting point for loading images, which we will see later.

Here’s a summary of the Fresco DataSource concept and its role: In Fresco the DraweeController creates a DataSource every time it initiates an image load, which provides the requested data (image). The DataSource is just an interface, but the loading process is implemented by the ProducerSequence Fresco.

Fresco image logic before loading

With that in mind, let’s go through the image-loading source code (from UI to DraweeController) to sort out the connections between the modules as we know them so far. When we use Fresco “loading images are generally use this API: SimpleDraweeView. SetImageURI (imageLink), this method will eventually call to:

// SimpleDraweeView.java public void setImageURI(Uri uri, @Nullable Object callerContext) { DraweeController controller = mControllerBuilder .setCallerContext(callerContext) .setUri(uri) .setOldController(getController()) .build(); // Controller setController(controller); } public void setController(@Nullable DraweeController draweeController) { mDraweeHolder.setController(draweeController); super.setImageDrawable(mDraweeHolder.getTopLevelDrawable()); }Copy the code

DraweeControllerBuilder is used to build a DraweeController for each load. The DraweeController is multiplexed by default, and this multiplexing is for the same SimpleDraweeView

. The DraweeController is then set to the DraweeHolder and the TopLevelDrawable is fetched from the DraweeHolder by default at the start of loading and displayed to the DraweeView. Continuing with DraweeHolder’s logic:

// DraweeHolder.java
public @Nullable Drawable getTopLevelDrawable() {
    return mHierarchy == null ? null : mHierarchy.getTopLevelDrawable();
}
Copy the code
/** Sets a new controller. */ public void setController(@Nullable DraweeController draweeController) { boolean wasAttached = mIsControllerAttached; if (wasAttached) { detachController(); } // Clear the old controller if (isControllerValid()) { mEventTracker.recordEvent(Event.ON_CLEAR_OLD_CONTROLLER); mController.setHierarchy(null); } mController = draweeController; If (wasAttached) {attachController(); }}Copy the code

In DraweeHolder. SetController () give DraweeController DraweeHierachy Settings, and attachController (), ` ` attachController () mainly call DraweeController. OnAttach () :

// AbstractDraweeController.java public void onAttach() { ... mIsAttached = true; if (! mIsRequestSubmitted) { submitRequest(); } } protected void submitRequest() { mDataSource = getDataSource(); Final DataSubscriber<T> DataSubscriber = new BaseDataSubscriber<T>() OnNewResultImpl (DataSource<T> DataSource) { }... }; . mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor); / / mUiThreadImmediateExecutor refers to dataSubscriber callback methods running thread, this is the main thread}Copy the code

Submit a request via submitRequest(), which we’ve already seen, and the main thing it does is construct a DataSource. The DataSource we passed the tracking and its instance is actually CloseableProducerToDataSourceAdapter. CloseableProducerToDataSourceAdapter at construction time is called producer. ProduceResults (…). And then launch the whole picture loading process.

Summarize the image-loading logic from SimpleDraweeView->DraweeController with the following diagram:

Now that we’ve covered the logic of Fresco before it actually starts loading images, how does the Fresco image loading process work? What steps did you go through?

Producer

In Fresco, the in-memory caching, decoding, encoding, disk caching, and network requests are all implemented there. The basic unit of Fresco implementations is Producer.

Take a look at its definition:

/** * <p> Execution of image request consists of multiple different tasks such as network fetch, * disk caching, memory caching, decoding, applying transformations etc. Producer<T> represents * single task whose result is an instance of T. Breaking entire request into sequence of * Producers allows us to construct different requests while reusing the same blocks. */ public interface Producer<T> { /** * Start producing results for given context. Provided consumer is notified whenever progress  is made (new value is ready or error occurs). */ void produceResults(Consumer<T> consumer, ProducerContext context); }Copy the code

The role of a Producer can be defined as follows: A Producer handles a step in the Fresco process, such as getting the picture from the network, getting the picture from memory, decoding the picture, and so on. A Consumer is a listener. Let’s look at its definition:

public interface Consumer<T> { /** * Called by a producer whenever new data is produced. This method should not throw an  exception. * * <p>In case when result is closeable resource producer will close it after onNewResult returns. * Consumer needs to make copy of it if the resource must be accessed after that. Fortunately, * with CloseableReferences, that should not impose too much overhead. * * @param newResult * @param status bitwise values describing the returned result * @see Status for status flags */ void onNewResult(T newResult, @Status int status); /** * Called by a producer whenever it terminates further work due to Throwable being thrown. This * method should not throw an exception. * * @param t */ void onFailure(Throwable t); /** Called by a producer whenever it is cancelled and won't produce any more results */ void onCancellation(); /** * Called when the progress updates. * * @param progress in range [0, 1] */ void onProgressUpdate(float progress); }Copy the code

The result of the Producer’s processing can be used by the Consumer to tell the outside world, for example, whether the Producer failed or succeeded.

The combination of the Producer

One ProducerA can accept another ProducerB as a parameter, and if ProducerA is finished, ProducerB can be called to continue processing. And pass Consumer to observe ProducerB processing results. For example, Fresco loads images from the in-memory cache, or loads them from the network if they are not in the in-memory cache. Here involves two Producer BitmapMemoryCacheProducer and NetworkFetchProducer, assuming BitmapMemoryCacheProducer ProducerA, NetworkFetchProducer ProducerB. Let’s look at their logic in pseudocode:

// BitmapMemoryCacheProducer.java public class BitmapMemoryCacheProducer implements Producer<CloseableReference<CloseableImage>> { private final Producer<CloseableReference<CloseableImage>> mInputProducer; / / we assume inputProducer here for NetworkFetchProducer public BitmapMemoryCacheProducer (... ,Producer<CloseableReference<CloseableImage>> inputProducer) { ... mInputProducer = inputProducer; } @Override public void produceResults(Consumer<CloseableReference<CloseableImage>> consumer,...) { CloseableReference<CloseableImage> cachedReference = mMemoryCache.get(cacheKey); if (cachedReference ! = null) {/ / from the cache to be successful, directly to inform the outside world. Consumer onNewResult (cachedReference, BaseConsumer simpleStatusForIsLast (isFinal)); return; Consumer<CloseableReference<CloseableImage>> wrappedConsumer = wrapConsumer(Consumer..) ; / / package with a layer of Consumer, namely mInputProducer results, it can be observed mInputProducer. ProduceResults (wrappedConsumer producerContext); // Network loading}}Copy the code

 

/ / NetworkFetchProducer. Java public class NetworkFetchProducer implements Producer < EncodedImage > {/ / it is not InputProducer, if the network fails to get Fresco images, @override public void produceResults(final Consumer<CloseableReference<CloseableImage>> Consumer) {// network access //... If (get network picture){notifyConsumer(...) ; // Notify the consumer of the result}... }}Copy the code

The code may not be easy to understand, but you can use the following diagram to understand the relationship:

Fresco can flexibly define different picture processing flows by assembling producers into a ProducerSequence (there is no such class). A ProducerSequence generally defines a picture processing process. For example, a ProducerSequence that loads pictures from the network is called NetworkFetchSequence and contains multiple types of Producer.

Network image loading process

Different image requests in Fresco have different ProducerSequence processes, such as web image requests:

// ProducerSequenceFactory.java private Producer<CloseableReference<CloseableImage>> getBasicDecodedImageSequence(ImageRequest imageRequest) { switch (imageRequest.getSourceUriType()) { case SOURCE_TYPE_NETWORK: return getNetworkFetchSequence(); . }Copy the code

So for network image requests getNetworkFetchSequence is called:

/** * swallow result if prefetch -> bitmap cache get -> background thread hand-off -> multiplex -> * bitmap cache -> decode -> multiplex -> encoded cache -> disk cache -> (webp transcode) -> * network fetch. */ private synchronized Producer<CloseableReference<CloseableImage>> getNetworkFetchSequence() { ... mNetworkFetchSequence = new BitmapCacheGetToDecodeSequence(getCommonNetworkFetchToEncodedMemorySequence()); . return mNetworkFetchSequence; }Copy the code

The getNetworkFetchSequence uses multiple calls to compose multiple producers. Instead of following the code logic, I’ll use the following diagram to illustrate the Fresco network image loading process:

 

You can see how complex the Fresco image loading process is. In the figure above, I just list some key producers. In fact, I haven’t drawn some of them.

conclusion

To aid in understanding, a summary flow chart is provided that covers the entire process above. The next series of articles will cover the UI and image loading process in more detail, and hopefully read the source code to get a better understanding of the internal code logic and design ideas.

In fact, when we read other people’s source code, in addition to know the specific details, but also to pay attention to other people’s module design, learn from its design ideas. Then think about how you would divide the modules and how you would connect the different modules if you were designing them.

When modules are divided, how are the sub-modules divided and how are the cooperation between them maintained?

 

Refer to the article

Android open-source framework source code: Fresco

Appreciation of Fresco architecture design