Glide decryption

Keywords: Fragment network encoding calculator

Glide is now the most widely used image loading framework, and has always wanted to work on it. Every time, it is deeply involved in it... This time, I will make a thorough analysis of it and try to cover the whole process and its details.

This article's Glide parsing is based on the latest version 4.11.0.

In fact, when loading images from the general network, you can simply analyze the general process. It is nothing more than to establish relevant request information, then request the request information through thread pool technology, and then convert the downloaded image file to display.

Let's start with a simple test using code, and then drill down

Glide.with(view.getContext())
                .load(url)
                .into(view);

with()

Glide's with function provides us with different input parameters, and its final return object is RequestManager

Our test code uses Context, so let's trace this function here. In fact, the others are similar

  @NonNull
  public static RequestManager with(@NonNull Context context) {
    return getRetriever(context).get(context);
  }
  
 @NonNull
  private static RequestManagerRetriever getRetriever(@Nullable Context context) {
    //Verification Context cannot be empty
    Preconditions.checkNotNull(context,"....");
    return Glide.get(context).getRequestManagerRetriever();
  }
  //Get the requestManagerRetriever property of Glide object directly
  public RequestManagerRetriever getRequestManagerRetriever() {
    return requestManagerRetriever;
  }

You can see the creation of the RequestManagerRetriever object. It must have been processed in Glide.get()

  //Create Glide object by double lock singleton method
  public static Glide get(@NonNull Context context) {
    if (glide == null) {
      GeneratedAppGlideModule annotationGeneratedModule =
          getAnnotationGeneratedGlideModules(context.getApplicationContext());
      synchronized (Glide.class) {
        if (glide == null) {
          checkAndInitializeGlide(context, annotationGeneratedModule);
        }
      }
    }
    return glide;
  }
  //Verify and initialize Glide object
  @GuardedBy("Glide.class")
  private static void checkAndInitializeGlide(
      @NonNull Context context, @Nullable GeneratedAppGlideModule generatedAppGlideModule) {
    // In the thread running initGlide(), one or more classes may call Glide.get(context).
    // Without this check, those calls could trigger infinite recursion.
    if (isInitializing) {//If it is being created, an error will be reported directly
      throw new IllegalStateException(
          "You cannot call Glide.get() in registerComponents(),"
              + " use the provided Glide instance instead");
    }
    isInitializing = true;
    //Real creation method
    initializeGlide(context, generatedAppGlideModule);
    isInitializing = false;
  }
  //Initialize Glide
  @GuardedBy("Glide.class")
  private static void initializeGlide(
      @NonNull Context context, @Nullable GeneratedAppGlideModule generatedAppGlideModule) {
    initializeGlide(context, new GlideBuilder(), generatedAppGlideModule);
  }

  @GuardedBy("Glide.class")
  @SuppressWarnings("deprecation")
  private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder, @Nullable GeneratedAppGlideModule annotationGeneratedModule) {
    ...
    //The RequestManagerFactory factory object is created to create the corresponding RequestManager
    RequestManagerRetriever.RequestManagerFactory factory =
        annotationGeneratedModule != null
            ? annotationGeneratedModule.getRequestManagerFactory()
            : null;
    builder.setRequestManagerFactory(factory);
    ...
    //Builder Design pattern, create Glide object
    Glide glide = builder.build(applicationContext);
    ...
    applicationContext.registerComponentCallbacks(glide);
    Glide.glide = glide;
  }

As you can see, the builder design pattern is used here to create the glide object. A property of RequestManagerFactory is set in builder. Let's see what we did in builder.

  @NonNull
  Glide build(@NonNull Context context) {
    if (sourceExecutor == null) {//Create resource actuator
      sourceExecutor = GlideExecutor.newSourceExecutor();
    }
    if (diskCacheExecutor == null) {//Disk cache actuator
      diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();
    }
    if (animationExecutor == null) {//Animation actuator
      animationExecutor = GlideExecutor.newAnimationExecutor();
    }
    if (memorySizeCalculator == null) {//Memory size calculator, based on
      memorySizeCalculator = new MemorySizeCalculator.Builder(context).build();
    }
    if (connectivityMonitorFactory == null) {//Factory connected for monitoring
      connectivityMonitorFactory = new DefaultConnectivityMonitorFactory();
    }
    if (bitmapPool == null) {//bitmap pool
      int size = memorySizeCalculator.getBitmapPoolSize();
      if (size > 0) {
        bitmapPool = new LruBitmapPool(size);
      } else {
        bitmapPool = new BitmapPoolAdapter();
      }
    }
    if (arrayPool == null) {
      arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes());
    }
    if (memoryCache == null) {//Memory cache policy, Lru cache policy is used by default
      memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
    }
    if (diskCacheFactory == null) {//Hard disk cache policy
      diskCacheFactory = new InternalCacheDiskCacheFactory(context);
    }
    if (engine == null) {//Engine, which includes the created executor and cached information
      engine = new Engine(
              memoryCache,
              diskCacheFactory,
              diskCacheExecutor,
              sourceExecutor,
              GlideExecutor.newUnlimitedSourceExecutor(),
              animationExecutor,
              isActiveResourceRetentionAllowed);
    }

    if (defaultRequestListeners == null) {//Request listener, an immutable List
      defaultRequestListeners = Collections.emptyList();
    } else {
      defaultRequestListeners = Collections.unmodifiableList(defaultRequestListeners);
    }
    //Here, a RequestManagerRetriever object is created. The parameter is the Factory object set previously
    RequestManagerRetriever requestManagerRetriever = new RequestManagerRetriever(requestManagerFactory);
    return new Glide(//Create Glide object
        context,
        engine,
        memoryCache,
        bitmapPool,
        arrayPool,
        requestManagerRetriever,
        connectivityMonitorFactory,
        logLevel,
        defaultRequestOptionsFactory,
        defaultTransitionOptions,
        defaultRequestListeners,
        isLoggingRequestOriginsEnabled,
        isImageDecoderEnabledForBitmaps);
  }

In GlideBuilder, many objects are created for us, including thread pool, buffer, cache size, Engine and RequestManagerRetriever.

Because when we call with() method, we use requestManagerRetriever. Let's take a look and see if there is any special processing in it

public RequestManagerRetriever(@Nullable RequestManagerFactory factory) {
  this.factory = factory != null ? factory : DEFAULT_FACTORY;
  handler = new Handler(Looper.getMainLooper(), this /* Callback */);
}

private static final RequestManagerFactory DEFAULT_FACTORY =
      new RequestManagerFactory() {
        @NonNull
        @Override
        public RequestManager build(@NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode requestManagerTreeNode, @NonNull Context context) {
          return new RequestManager(glide, lifecycle, requestManagerTreeNode, context);
        }
      };

A very simple constructor, which creates the handler object and sets the RequestManagerFactory object.

Back in the trunk, take a look at Glide's constructor.

  Glide(
      @NonNull Context context,
      @NonNull Engine engine,
      @NonNull MemoryCache memoryCache,
      @NonNull BitmapPool bitmapPool,
      @NonNull ArrayPool arrayPool,
      @NonNull RequestManagerRetriever requestManagerRetriever,
      @NonNull ConnectivityMonitorFactory connectivityMonitorFactory,
      int logLevel,
      @NonNull RequestOptionsFactory defaultRequestOptionsFactory,
      @NonNull Map<Class<?>, TransitionOptions<?, ?>> defaultTransitionOptions,
      @NonNull List<RequestListener<Object>> defaultRequestListeners,
      boolean isLoggingRequestOriginsEnabled,
      boolean isImageDecoderEnabledForBitmaps) {
    this.engine = engine;
    this.bitmapPool = bitmapPool;
    this.arrayPool = arrayPool;
    this.memoryCache = memoryCache;
    this.requestManagerRetriever = requestManagerRetriever;
    this.connectivityMonitorFactory = connectivityMonitorFactory;
    this.defaultRequestOptionsFactory = defaultRequestOptionsFactory;

    final Resources resources = context.getResources();
    //Registration machine, which maintains various registry information of encoding, decoding, loading, picture request header, supported pictures, etc
    registry = new Registry();
    registry.register(new DefaultImageHeaderParser());
    registry
        .append(int.class, InputStream.class, resourceLoaderStreamFactory)
        .......
    ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory();
    glideContext =
        new GlideContext(//Create a glideContext object
            context,
            arrayPool,
            registry,
            imageViewTargetFactory,
            defaultRequestOptionsFactory,
            defaultTransitionOptions,
            defaultRequestListeners,
            engine,
            isLoggingRequestOriginsEnabled,
            logLevel);
  }

In this process, some properties are assigned, and the GlideContext object and the registry object are created.

So far, our Glide singleton object creation is complete

Getting the RequestManger object

public static RequestManager with(@NonNull Context context) {
  return getRetriever(context).get(context);
}

After getting the RequestManagerRetriever object, get the RequestManager object through the get method. Now let's trace the implementation of the code.

  @NonNull
  public RequestManager get(@NonNull Context context) {
    if (context == null) {//Throw exception if context is empty
      throw new IllegalArgumentException("You cannot start a load on a null Context");
    } else if (Util.isOnMainThread() && !(context instanceof Application)) {
      //If the current thread is the main thread and the context is not an Application, the corresponding life cycle is bound to the UI(Activity or Fragment),
      //Listen to the context lifecycle by creating a method to hide fragments. Then bind RequestManager and Fragment.
      //Because there are different ways to create fragments in v4 and ordinary fragments, different processing is carried out according to different context types
      if (context instanceof FragmentActivity) {//If it's FragmentActivity
        return get((FragmentActivity) context);
      } else if (context instanceof Activity) {//If it's a normal Activity
        return get((Activity) context);
      } else if (context instanceof ContextWrapper
          // Only unwrap a ContextWrapper if the baseContext has a non-null application context.
          // Context#createPackageContext may return a Context without an Application instance,
          // in which case a ContextWrapper may be used to attach one.
          && ((ContextWrapper) context).getBaseContext().getApplicationContext() != null) {
        return get(((ContextWrapper) context).getBaseContext());
      }
    }
    //Return to application RequestManager
    return getApplicationManager(context);
  }

In the get() method, different processing is performed according to the type of Context. We trace a FragmentActivity type here, and others are similar

  @NonNull
  public RequestManager get(@NonNull FragmentActivity activity) {
    if (Util.isOnBackgroundThread()) {//If it is a background thread, the context is processed according to the application, that is, it is unbound with the UI life cycle
      return get(activity.getApplicationContext());
    } else {
      //Processing in UI thread
      assertNotDestroyed(activity);//Determine whether the activity has been destroyed
      FragmentManager fm = activity.getSupportFragmentManager();
      return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
    }
  }
  //Get RequestManager managed by FragmentManager
  private RequestManager supportFragmentGet(@NonNull Context context, @NonNull FragmentManager fm, @Nullable Fragment parentHint,
      boolean isParentVisible) {
    //Create an invisible SupportRequestManagerFragment to listen to the lifecycle of the corresponding Context
    SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm, parentHint, isParentVisible);
    //Get the corresponding request manager in the Fragment (each Fragment has only one unique request manager)
    RequestManager requestManager = current.getRequestManager();
    if (requestManager == null) {//If the request manager is not currently set, create and set
      Glide glide = Glide.get(context);
      //Using the factory method, create a requestManager object
      requestManager = factory.build(glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
      current.setRequestManager(requestManager);
    }
    return requestManager;
  }

The actual request manager is built through factory

RequestManager(Glide glide, Lifecycle lifecycle, RequestManagerTreeNode treeNode,
    RequestTracker requestTracker, ConnectivityMonitorFactory factory, Context context) {
  this.glide = glide;
  this.lifecycle = lifecycle;
  this.treeNode = treeNode;
  this.requestTracker = requestTracker;
  this.context = context;
  //Connect monitor
  connectivityMonitor = factory
      .build(context.getApplicationContext(), new RequestManagerConnectivityListener(requestTracker));
  if (Util.isOnBackgroundThread()) {
    mainHandler.post(addSelfToLifecycle);
  } else {
    lifecycle.addListener(this);
  }
  lifecycle.addListener(connectivityMonitor);
  defaultRequestListeners =
      new CopyOnWriteArrayList<>(glide.getGlideContext().getDefaultRequestListeners());
  //Set request configuration information
  setRequestOptions(glide.getGlideContext().getDefaultRequestOptions());
  //Register RequestManager to global glide
  glide.registerRequestManager(this);
}

So far, we have created the RequestManager for, and then it calls the load() method.

load()

RequestManager supports image loading in various parameter forms:

We trace from our case, and the parameter passed in is of type String.

//It is equivalent to calling asDrawable() first and then calling the load() method.
  public RequestBuilder<Drawable> load(@RawRes @DrawableRes @Nullable Integer resourceId) {
    return asDrawable().load(resourceId);
  }

  public RequestBuilder<Drawable> asDrawable() {
    return as(Drawable.class);
  }

  //Create a RequestBuilder that can decode the corresponding type of pictures
  public <ResourceType> RequestBuilder<ResourceType> as(
      @NonNull Class<ResourceType> resourceClass) {
    return new RequestBuilder<>(glide, this, resourceClass, context);
  }

Let's see what the RequestBuilder's construction method does

  protected RequestBuilder(@NonNull Glide glide, RequestManager requestManager, 
      Class<TranscodeType> transcodeClass, Context context) {
    this.glide = glide;
    this.requestManager = requestManager;
    this.transcodeClass = transcodeClass;
    this.context = context;
    //The approximate rate of transcodeClass here is Drawable class object
    this.transitionOptions = requestManager.getDefaultTransitionOptions(transcodeClass);
    this.glideContext = glide.getGlideContext();

    initRequestListeners(requestManager.getDefaultRequestListeners());
    apply(requestManager.getDefaultRequestOptions());
  }

After the RequestBuilder object is created, the load() method of RequestBuilder is called directly.

  public RequestBuilder<TranscodeType> load(@Nullable String string) {
    return loadGeneric(string);
  }
  
  private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {
    this.model = model;
    isModelSet = true;
    return this;
  }

There is no special operation here, just set the isModelSet to true, and the tag model has been set.

into()

In the with() and load() methods, we mainly do some preparatory work. The real operations of loading, caching and converting images to View are all performed in the into().

  public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) {
    Util.assertMainThread();//Method needs to be executed on the main thread
    Preconditions.checkNotNull(view);//view cannot be empty
    BaseRequestOptions<?> requestOptions = this;
    if (!requestOptions.isTransformationSet()
        && requestOptions.isTransformationAllowed()
        && view.getScaleType() != null) {//Set the requestOptions according to the scaleType configured on the View
      switch (view.getScaleType()) {
        case CENTER_CROP:
          requestOptions = requestOptions.clone().optionalCenterCrop();
          break;
      }
    }
    return into(glideContext.buildImageViewTarget(view, transcodeClass),/*targetListener=*/ null, 
        requestOptions,
        Executors.mainThreadExecutor());
  }

Here, we configure the requestOptions related parameters according to the information set, and then call the buildImageViewTarget method to construct a viewTarget object.

Let's take a look at this method

  @NonNull
  public <X> ViewTarget<ImageView, X> buildImageViewTarget(
      @NonNull ImageView imageView, @NonNull Class<X> transcodeClass) {
    //The ViewTarget object is created by the factory method. Generally, DrawableImageViewTarget is returned here. If asBitmap is used, BitmapImageViewTarget is returned
    return imageViewTargetFactory.buildTarget(imageView, transcodeClass);
  }
  
public class ImageViewTargetFactory {
  public <Z> ViewTarget<ImageView, Z> buildTarget( @NonNull ImageView view, @NonNull Class<Z> clazz) {
    if (Bitmap.class.equals(clazz)) {//If the asBitmap method is used, then the clazz here is bitmap
      return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view);
    } else if (Drawable.class.isAssignableFrom(clazz)) {//If it is only used normally, DrawableImageViewTarget will be returned
      return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view);
    } else {
      throw new IllegalArgumentException("Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)");
    }
  }
}

As you can see here, in most cases, a DrawableImageViewTarget object information is returned,

Back to the main line. Continue

  private <Y extends Target<TranscodeType>> Y into(@NonNull Y target, @Nullable RequestListener<TranscodeType> targetListener,
      BaseRequestOptions<?> options, Executor callbackExecutor) {
    Preconditions.checkNotNull(target);
    if (!isModelSet) {//If the model is not set (a method that does not call the load method will cause this), an error is reported directly.
      throw new IllegalArgumentException("You must call #load() before calling #into()");
    }
    //Create a request
    Request request = buildRequest(target, targetListener, options, callbackExecutor);
    //Get whether the corresponding request already exists on the target, and if so, clear it
    Request previous = target.getRequest();
    if (request.isEquivalentTo(previous) && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
      //The request of the mark point on the target is the same as the generated request. Start the corresponding request directly, and then return the target object
      if (!Preconditions.checkNotNull(previous).isRunning()) {
        previous.begin();
      }
      return target;
    }
    //Remove the original target from the request management class requestManager
    requestManager.clear(target);
    //Set new request to target
    target.setRequest(request);
    //Request manager enables tracking of target and request (mainly to start the request and manage the request class and target in a unified way)
    requestManager.track(target, request);
    return target;
  }

This code is the core of Glide, which creates and executes the Request object.

Creation process of request object

Let's first look at the creation process of Request object: buildRequest()

  private Request buildRequest(Target<TranscodeType> target, @Nullable RequestListener<TranscodeType> targetListener,
      BaseRequestOptions<?> requestOptions, Executor callbackExecutor) {
    return buildRequestRecursive(/*requestLock=*/ new Object(), target, targetListener,
        /*parentCoordinator=*/ null, transitionOptions, requestOptions.getPriority(),
        requestOptions.getOverrideWidth(), requestOptions.getOverrideHeight(), requestOptions, callbackExecutor);
  }
  
  //Create a request class, which is divided into processing the request of error display and the request of normal display
  private Request buildRequestRecursive(Object requestLock, Target<TranscodeType> target,
      @Nullable RequestListener<TranscodeType> targetListener, @Nullable RequestCoordinator parentCoordinator,
      TransitionOptions<?, ? super TranscodeType> transitionOptions, Priority priority,
      int overrideWidth, int overrideHeight,
      BaseRequestOptions<?> requestOptions, Executor callbackExecutor) {
    .....
    Request mainRequest =//Request for normal display
        buildThumbnailRequestRecursive(requestLock, target, targetListener, parentCoordinator, transitionOptions,
            priority, overrideWidth, overrideHeight, requestOptions, callbackExecutor);
    ...
    return errorRequestCoordinator;
  }

This function classifies the requests. One is the error displayed request information, and the other is the normal displayed request information.

Let's take a look at how it is created in the normally displayed request processing function.

  //Generate and process the request of normal display, and create thumbnail or original image according to relevant settings
  private Request buildThumbnailRequestRecursive(Object requestLock,Target<TranscodeType> target,
      RequestListener<TranscodeType> targetListener,@Nullable RequestCoordinator parentCoordinator,
      TransitionOptions<?, ? super TranscodeType> transitionOptions,Priority priority,
      int overrideWidth,int overrideHeight,BaseRequestOptions<?> requestOptions,Executor callbackExecutor) {
    if (thumbnailBuilder != null) {//There is a thumbnail generator to create a thumbnail request based on builder information
      ...
    } else if (thumbSizeMultiplier != null) {//If there is image zoom index, create thumbnail request based on index information
      ...
    } else {
      //Generally, it will be handled in this way
      return obtainRequest(requestLock, target, targetListener, requestOptions, parentCoordinator,
          transitionOptions, priority, overrideWidth, overrideHeight, callbackExecutor);
    }
  }

This code segment is very long. We omitted it. Most of it is special processing for generating thumbnail request. We have a chance to analyze it later. Let's take a look at how the obtainRequest method creates standard requests.

  private Request obtainRequest(Object requestLock, Target<TranscodeType> target,
      RequestListener<TranscodeType> targetListener, BaseRequestOptions<?> requestOptions,
      RequestCoordinator requestCoordinator, TransitionOptions<?, ? super TranscodeType> transitionOptions,
      Priority priority, int overrideWidth, int overrideHeight, Executor callbackExecutor) {
    return SingleRequest.obtain(context, glideContext, requestLock, model, transcodeClass,
        requestOptions, overrideWidth, overrideHeight, priority, target, targetListener,
        requestListeners, requestCoordinator, glideContext.getEngine(), transitionOptions.getTransitionFactory(),
        callbackExecutor);
  }

This method continues to trace. It can be found that only a SingleRequest object has been created and related parameters have been assigned. There is no special processing in it.

So far, our Request has been created. The next step is to see how network requests are made.

Execution of request

Back in the original into() code block, this time we are tracking the function requestManager.track(target, request)

  synchronized void track(@NonNull Target<?> target, @NonNull Request request) {
    //Target management class, in which all target information is saved through set list, and the life cycle interface is implemented, which can start or pause target animation according to the life cycle
    targetTracker.track(target);
    //requestTracker management class, in which all the request information is saved by set list, the request is saved to set list by runRequest method, and the request is started
    requestTracker.runRequest(request);
  }
  //Start and track requests
  public void runRequest(@NonNull Request request) {
    requests.add(request);//Save to set list
    if (!isPaused) {//Load is available, call begin() directly
      request.begin();
    } else {//When the load is in a suspended state and the request request is saved, it will be started one by one from the list when it is ready
      request.clear();
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Paused, delaying request");
      }
      //Put the request in pending requests, because requests are weak references, preventing them from being recycled
      pendingRequests.add(request);
    }
  }

This is a simple process. If the current RequestManager is available (that is, the bound Context is visible), the task request processing will be performed directly. Otherwise, the request will be placed in the set list corresponding to pendingRequests. Although the corresponding request information has been saved in requests, it will be recycled because it is a weak reference, so pendingRequests is used for saving.

In the analysis of creating the request object, we know that the last one created is the SingleRequest object, and continue to trace the begin() method

 public void begin() {
    synchronized (requestLock) {
      assertNotCallingCallbacks();
      stateVerifier.throwIfRecycled();
      startTime = LogTime.getLogTime();
      if (model == null) {//If the loaded image source (url,file, etc.) is null, onLoadFailed will be used directly
        ...
        onLoadFailed(new GlideException("Received null model"), logLevel);
        return;
      }
      if (status == Status.RUNNING) {//If the request is in progress, throw the exception directly
        throw new IllegalArgumentException("Cannot restart a running request");
      }
      if (status == Status.COMPLETE) {//If the request has been completed, call the onResourceReady interface directly
        //If we do it after a reboot (usually through a notifyDataSetChanged to start the same request to the same target or view),
        //We can simply use resources and sizes without having to get a new size, start a new load, etc.
        // This does mean that if the customer does need to reload, the displayed call clear clears the view or target before loading.
        onResourceReady(resource, DataSource.MEMORY_CACHE);
        return;
      }
      status = Status.WAITING_FOR_SIZE;
      if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        //If the width and height of the view to be loaded are fixed, load it directly
        onSizeReady(overrideWidth, overrideHeight);
      } else {
        //Set the callback information. When the width and height of the View are finished, the callback processing is performed. Through the getViewTreeObserver method of target(Target class, which contains the information of View), monitor the drawing of the control, so as to obtain the corresponding width and height. Finally, call the onSizeReady(width,height) method through the interface callback
        target.getSize(this);
      }
      if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
          && canNotifyStatusChanged()) {
        //Set the PlaceholderDrawable information, load the resources in the background, and display the corresponding data on the target
        target.onLoadStarted(getPlaceholderDrawable());
      }
      if (IS_VERBOSE_LOGGABLE) {
        logV("finished run method in " + LogTime.getElapsedMillis(startTime));
      }
    }
  }

In the begin() function, the main function is to call the onSizeReady method to perform the specific loading process when the View width and height are known (whether fixed or through the drawn callback). In addition, when the loading is not completed, the corresponding bitmap is set on the View (that is, the. placeholder() method that we often use in the code).

  public void onSizeReady(int width, int height) {
    stateVerifier.throwIfRecycled();
    synchronized (requestLock) {
      ...
      float sizeMultiplier = requestOptions.getSizeMultiplier();//Width height reprocessing according to scale
      this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
      this.height = maybeApplySizeMultiplier(height, sizeMultiplier);
      ...
      loadStatus =
          engine.load(//To load
              glideContext,
              model,
              requestOptions.getSignature(),
              this.width,
              this.height,
              requestOptions.getResourceClass(),
              transcodeClass,
              priority,
              requestOptions.getDiskCacheStrategy(),
              requestOptions.getTransformations(),
              requestOptions.isTransformationRequired(),
              requestOptions.isScaleOnlyOrNoTransform(),
              requestOptions.getOptions(),
              requestOptions.isMemoryCacheable(),
              requestOptions.getUseUnlimitedSourceGeneratorsPool(),
              requestOptions.getUseAnimationPool(),
              requestOptions.getOnlyRetrieveFromCache(),
              this,
              callbackExecutor);
       ...
    }
  }

The function finally performs the loading process by calling engine.load.

  //Real load, processed through multi-level cache
  public <R> LoadStatus load(
      GlideContext glideContext,
      Object model,
      Key signature,
      int width,
      int height,
      Class<?> resourceClass,
      Class<R> transcodeClass,
      Priority priority,
      DiskCacheStrategy diskCacheStrategy,
      Map<Class<?>, Transformation<?>> transformations,
      boolean isTransformationRequired,
      boolean isScaleOnlyOrNoTransform,
      Options options,
      boolean isMemoryCacheable,
      boolean useUnlimitedSourceExecutorPool,
      boolean useAnimationPool,
      boolean onlyRetrieveFromCache,
      ResourceCallback cb,
      Executor callbackExecutor) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

    EngineKey key =
        keyFactory.buildKey(//Create a key value according to the relevant parameters
            model,
            signature,
            width,
            height,
            transformations,
            resourceClass,
            transcodeClass,
            options);

    EngineResource<?> memoryResource;
    synchronized (this) {
      //Load from memory
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
      if (memoryResource == null) {
        //If there is no query in memory, create a Job internally and execute it, return LoadStatus
        return waitForExistingOrStartNewJob(
            glideContext,
            model,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            options,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache,
            cb,
            callbackExecutor,
            key,
            startTime);
      }
    }

As we all know, any existing image loading framework uses cache to process data, so as to speed up image loading. Glide is no exception. You can see the loading of resources from the code.

A key is generated through related parameters

  1. Search in memory through key
  2. If it is found, the resource information is returned directly through the interface callback.
  3. If not, a new task request is created to load.

Let's start with the memory loading function loadFromMemory() to analyze how it is handled.

  private EngineResource<?> loadFromMemory(EngineKey key, boolean isMemoryCacheable, long startTime) {
    if (!isMemoryCacheable) {//If it is not allowed to load in the cache, directly return to turn off the cache function through. skipMemoryCache(false)
      return null;
    }
    //Get from the list of pictures in use
    EngineResource<?> active = loadFromActiveResources(key);
    if (active != null) {
      return active;
    }
    //Get from memorycache (usually lurmemorycache is used here)
    EngineResource<?> cached = loadFromCache(key);
    if (cached != null) {
      return cached;
    }
    return null;
  }

  //Loaded from resources in use
  private EngineResource<?> loadFromActiveResources(Key key) {
    //Get from loaded resources
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {//Resource consuming counter + 1
      active.acquire();
    }
    return active;
  }
  //Get from cache
  private EngineResource<?> loadFromCache(Key key) {
    //Get from Cache
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      //If it exists in the cache, it is removed from the cache and placed in the list of resources being used
      cached.acquire();
      activeResources.activate(key, cached);
    }
    return cached;
  }

The source code is relatively simple.

  1. loadFromActiveResources() first looks up the resources in use. If found, the resource's reference counter + 1 is returned to the resource file.
  2. If it is not found, it is obtained from the cache, and the lrumemorycache used by the cache by default (can be used). If successful, it is removed from the cache and placed in active resources (strong reference). Prevent resource invalidation caused by cache being recycled.
  3. If not, the entire function returns null

If the resource file is not retrieved from memory, the code will load the resource through waitForExistingOrStartNewJob.

  //Use existing jobs or create new jobs to load resources
  private <R> LoadStatus waitForExistingOrStartNewJob(...) {
    //From the list of jobs executed, onlyRetrieveFromCache is to load picture flag bits from cache only.
    EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
    if (current != null) {
      //If it exists, add a callback interface for the current task,
      //It is possible that a resource is used in multiple places at the same time. Therefore, after loading, multiple interfaces need to be called back for notification
      current.addCallback(cb, callbackExecutor);
      return new LoadStatus(cb, current);
    }
    //Create an EngineJob
    EngineJob<R> engineJob =engineJobFactory.build(...);
    //It is responsible for obtaining data from cached data resources or original resources. It is a data acquisition class
    DecodeJob<R> decodeJob =decodeJobFactory.build(...);
    //Store the engineJob in jobs. There is a field of onlyRetrieveFromCache in engineJob, so it can be placed in different list s according to the field
    jobs.put(key, engineJob);
    //Add callback interface
    engineJob.addCallback(cb, callbackExecutor);
    //Start engineJob to load resources
    engineJob.start(decodeJob);
    return new LoadStatus(cb, engineJob);
  }

We are only concerned about the process of creating a new EngineJob and executing the load, so here we mainly look at the execution of engineJob.start(decodeJob)

  public synchronized void start(DecodeJob<R> decodeJob) {
    this.decodeJob = decodeJob;
    //According to the setting of decodeJob, use the non saved actuator
    GlideExecutor executor = decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();
    //Start the decodeJob, which implements the Runnable interface and calls its run() method
    executor.execute(decodeJob);
  }

Let's now look at the run method of DecodeJob

  @Override
  public void run() {
    DataFetcher<?> localFetcher = currentFetcher;
    try {
      if (isCancelled) {//If it has been cancelled, it will directly return to failure. isCancelled is volatile. Ensure the visibility of multithreading
        notifyFailed();//Inform the upper layer (EngineJob) that the call fails, and recycle the related resources
        return;
      }
      //Main load function
      runWrapped();
    } catch (CallbackException e) {
      //If we have already called the callback upper interface when we have entered the encode phase, we need to release resources by calling the failed interface. Otherwise, it is not safe
      // You can check the notifyEncodeAndRelease(Resource, DataSource) method
      if (stage != Stage.ENCODE) {
        throwables.add(t);
        notifyFailed();
      }
      throw t;
    } finally {
      if (localFetcher != null) {//Clean up the localFetcher because the DecodeJob is reused.
        localFetcher.cleanup();
      }
      GlideTrace.endSection();//Record tracking information of the entire Glide
    }
  }

The run method has only one main function, runWrapped().

  private void runWrapped() {
    switch (runReason) {
      case INITIALIZE://If it is the initialization phase
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }

  //Data resource acquisition generator, initialization - > resource cache decoding - > data cache - > source - > end
  // According to different stages, different data loaders are generated
  private DataFetcherGenerator getNextGenerator() {
    switch (stage) {
      case RESOURCE_CACHE://Load data from cache file (including resources after down sampling / conversion)
        return new ResourceCacheGenerator(decodeHelper, this);
      case DATA_CACHE://Data cache load data (original cache resource)
        return new DataCacheGenerator(decodeHelper, this);
      case SOURCE://Source address load data of resource
        return new SourceGenerator(decodeHelper, this);
      case FINISHED:
        return null;
      default:
        throw new IllegalStateException("Unrecognized stage: " + stage);
    }
  }
  /**
   * Returns to the next state, based on the current state. Because diskCacheStrategy uses AUTOMATIC's caching policy by default,
   * decodeCachedResource()And decodeCachedData() return true
   * Next phase diagram:
   * Initialization - > resource cache decoding - > data cache - > source - > end
   *
   */
  private Stage getNextStage(Stage current) {
    switch (current) {
      case INITIALIZE:
        return diskCacheStrategy.decodeCachedResource()? Stage.RESOURCE_CACHE: getNextStage(Stage.RESOURCE_CACHE);
      case RESOURCE_CACHE:
        return diskCacheStrategy.decodeCachedData()? Stage.DATA_CACHE: getNextStage(Stage.DATA_CACHE);
      case DATA_CACHE:
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
  }

The main processes of Glide's resource loading are as follows

  1. Resource cache decoding
  2. Data cache
  3. Data source

The corresponding data loading classes are:

  1. ResourceCacheGenerator: load data from cache file (including resources after down sampling / conversion)
  2. DataCacheGenerator: load data from data cache (original cache resource)
  3. SourceGenerator: load data from the source address of the resource

Depending on the user's actual configuration information, one or more steps in the middle may be skipped.

After creating the data loading class, the relevant data loading is started through the runGenerators() * method.

  private void runGenerators() {
    currentThread = Thread.currentThread();
    startFetchTime = LogTime.getLogTime();
    boolean isStarted = false;
    //Stop cycle condition: canceled, current data loader is not empty, and current loader is not loaded to related resources
    while (!isCancelled && currentGenerator != null && !(isStarted = currentGenerator.startNext())) {
      stage = getNextStage(stage);
      currentGenerator = getNextGenerator();
      if (stage == Stage.SOURCE) {//If it is obtained from the resource source, enter the relevant scheduling of the source acquisition
        reschedule();
        return;
      }
    }
    if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {//If it is finished or cancelled, the direct callback fails
      notifyFailed();
    }
  }

In this function, the relevant data loader is traversed and executed through the while loop until all data loaders have been executed or some loader has loaded the relevant data.

startNext

Let's first look at how the ResourceCacheGenerator performs the load process.

 public boolean startNext() {
    //Each model corresponds to multiple parsers. Finally, according to the format of the model (String,Uri, etc.), find the LoadData list that can be used. Each LoadData will have a corresponding key.
    //The corresponding key list is obtained here
    List<Key> sourceIds = helper.getCacheKeys();//If the model does not have a corresponding mapped key. Then return false directly
    if (sourceIds.isEmpty()) {
      return false;
    }
    //Get the resource class information that can be decode d from model to resource
    List<Class<?>> resourceClasses = helper.getRegisteredResourceClasses();
    if (resourceClasses.isEmpty()) {
      if (File.class.equals(helper.getTranscodeClass())) {
        return false;
      }
      throw new IllegalStateException(
          "Failed to find any load path from "
              + helper.getModelClass()
              + " to "
              + helper.getTranscodeClass());
    }
    while (modelLoaders == null || !hasNextModelLoader()) {//Double traversal
      resourceClassIndex++;
      if (resourceClassIndex >= resourceClasses.size()) {
        sourceIdIndex++;
        if (sourceIdIndex >= sourceIds.size()) {
          return false;
        }
        resourceClassIndex = 0;
      }
      Key sourceId = sourceIds.get(sourceIdIndex);
      Class<?> resourceClass = resourceClasses.get(resourceClassIndex);
      Transformation<?> transformation = helper.getTransformation(resourceClass);se.
      currentKey =
          new ResourceCacheKey( // NOPMD Avoid Instantiating Objects InLoops
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
      //Get the cached file according to the current key
      cacheFile = helper.getDiskCache().get(currentKey);
      if (cacheFile != null) {//Get the cache file, set the corresponding modelLoader and key, and it will jump out of the loop
        sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }
    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData = modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        //There is corresponding LoadData
        started = true;
        //Load the corresponding file through the fetcher
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

Let's summarize the general process:

1. Obtain the corresponding sourceIds and resourceClasses according to the parameters passed in

2. Orthogonal traversal, iterating each group. Generate the key used in the cache file according to the relevant parameters, and then query whether there is a cache file according to the key

3. If the corresponding cache file is found, set sourceKey and modelLoaders. You can then jump out of the loop with these two settings.

4. modeLoaders traverse, get modelLoader and create corresponding loadData.

5. If the LoadData exists and the data class obtained by its internal datafetcher (data reader) exists, set the started flag bit to indicate that it has been loaded into the relevant data from ResourceCacheGenerator (so that all subsequent loaders will no longer execute). Then get the data through datafetcher.

6. If the traverse does not load relevant data, the returned started flag bit is false. Indicates that the ResourceCacheGenerator is not loaded into the related resource. After that, the loader (datacache generator, or SourceGenerator, or out of loop, depending on the previous settings) will continue to execute.

If it is loaded for the first time, the resource cannot be obtained in the DataCacheGenerator. Then the next one will execute the startNext() method of the DataCacheGenerator

startNext

  @Override
  public boolean startNext() {
    while (modelLoaders == null || !hasNextModelLoader()) {
      sourceIdIndex++;
      if (sourceIdIndex >= cacheKeys.size()) {
        return false;
      }
      //Get Key information of source resource
      Key sourceId = cacheKeys.get(sourceIdIndex);//cacheKeys=helper.getCacheKeys()
      //Obtain the corresponding original Key according to the current sourceId.
      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
      //Get the cache file from the disk cache according to the original Key, and jump out of the loop
      cacheFile = helper.getDiskCache().get(originalKey);
      if (cacheFile != null) {
        this.sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }
    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData = modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        //There is corresponding LoadData
        started = true;
        //Load the corresponding file through the fetcher
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

The main purpose of DataCacheGenerator is to get the original cache file. You can see that the general process is similar to ResourceCacheGenerator. The only difference is that the Key obtained is different. When obtaining the cache file, the parameter used is DataCacheKey, which is the Key value of the original cache file.

When loading for the first time, this must not be retrieved, and the returned one is empty. At this time, the next one will execute to the startNext() method of SourceGenerator.

startNext

  public boolean startNext() {
    if (dataToCache != null) {
      //After the first resource loading, a thread switch will be performed, and this method will be called again. At this time, dataToCache is not empty, and data will be saved. This is the source file of disk cache
      // And generated the DataCacheGenerator class
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }
    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      //In the case of a DataCacheGenerator class, the startNext method is called to load the file from the source file
      return true;
    }
    sourceCacheGenerator = null;
    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      //Cycle to get all the modelLoaders related to the current model and get the loadData data
      loadData = helper.getLoadData().get(loadDataListIndex++);
      //It should be noted here that if the cache policy thinks that data can be cached, then you do not need to manage the loadPath behind, and get the data first.
      //Since the cache will be handed over to the DataCacheGenerator for processing, you can skip the hasLoadPath judgment later.
      //hasLoadPath is based on the registered decoder in the Registry, and the converter judges whether the transformation of data class - > resource class - > transcodeclass can be completed.
      if (loadData != null &&
          (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
              || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        startNextLoad(loadData);
      }
    }
    return started;
  }

  private void startNextLoad(final LoadData<?> toStart) {
    loadData.fetcher.loadData(//Load resources
        helper.getPriority(),
        new DataCallback<Object>() {
          @Override
          public void onDataReady(@Nullable Object data) {
            if (isCurrentRequest(toStart)) {
              onDataReadyInternal(toStart, data);
            }
          }

          @Override
          public void onLoadFailed(@NonNull Exception e) {
            if (isCurrentRequest(toStart)) {
              onLoadFailedInternal(toStart, e);
            }
          }
        });
  }

When the resource loading is completed, the onDataReady interface will be called back. Let's take a look at the implementation of onDataReadyInternal.

 @Synthetic
  void onDataReadyInternal(LoadData<?> loadData, Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      //If the DataSource corresponding to DataFetcher can be cached
      dataToCache = data;
      //Here, cb refers to the DecodeJob class - > callback.reschedule (this), and callback refers to the EngineJob class
      //->getActiveSourceExecutor().execute(job);
      //That is, through the thread pool, the EngineJob is executed again, and then the startNext method of this class will be executed. Because dataToCache is not empty, the code block inside will be executed
      cb.reschedule();
    } else {
      //If caching is not possible, the onDataFetcherReady method is called directly
      cb.onDataFetcherReady(
          loadData.sourceKey, data, loadData.fetcher, loadData.fetcher.getDataSource(), originalKey);
    }
  }

Let's first analyze the first situation.

We wrote the comments in detail, and we know that we will call the startNext method of this class again in the end. Let's take a look at the beginning of this method.

    if (dataToCache != null) {
      //After the first resource loading, a thread switch will be performed, and this method will be called again. At this time, dataToCache is not empty, and data will be saved. This is the source file of disk cache
      // And generated the DataCacheGenerator class
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }
    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      //In the case of a DataCacheGenerator class, the startNext method is called to load the file from the source file
      return true;
    }
   
  private void cacheData(Object dataToCache) {
    long startTime = LogTime.getLogTime();
    try {
      //Get the encoder, through traversing the encoder list in the registry, get the encoder of the corresponding cache class
      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
      //generate
      DataCacheWriter<Object> writer = new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
      //Generate the key of the original cache file for encoding lookup in the DataCacheGenerator
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
      //Data caching through DiskCache
      helper.getDiskCache().put(originalKey, writer);
    } finally {
      loadData.fetcher.cleanup();
    }

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

That is, first save the original file on the disk, and then load it through the DataCacheGenerator class.

Now let's look at the second situation:

  public void onDataFetcherReady(
      Key sourceKey, Object data, DataFetcher<?> fetcher, DataSource dataSource, Key attemptedKey) {
    ...
    if (Thread.currentThread() != currentThread) {
      //When you set the execution reason to decode data and call unwrapper again, you can directly execute the decoding phase. This is the decodeFromRetrievedData method in else
      runReason = RunReason.DECODE_DATA;
      callback.reschedule(this);
    } else {
      GlideTrace.beginSection("DecodeJob.decodeFromRetrievedData");
      try {
        decodeFromRetrievedData();
      } finally {
        GlideTrace.endSection();
      }
    }
  }

At this time, a new call will be made according to the thread, or the decodeFromRetrievedData() method will be called directly.

 //Decoding retrieved data
  private void decodeFromRetrievedData() {
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      logWithTimeAndKey(
          "Retrieved data",
          startFetchTime,
          "data: "
              + currentData
              + ", cache key: "
              + currentSourceKey
              + ", fetcher: "
              + currentFetcher);
    }
    Resource<R> resource = null;
    try {
      //Decoding resources
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
      e.setLoggingDetails(currentAttemptingKey, currentDataSource);
      throwables.add(e);
    }
    if (resource != null) {
      //Through the interface callback, the resource is loaded successfully, and then the layer by layer callback is performed in decodejon - > enginejob - > Target. Finally, the Target operates ImageView to render the resource to View
      notifyEncodeAndRelease(resource, currentDataSource);
    } else {
      runGenerators();
    }
  }

The general process is basically completed. The details will be added a little bit later.

Add several documents:

Modelloader (resource loading class): contains two methods

buildLoadData(): create LoadData class

handles(Model): whether the class can load the given model

LoadData class: contains 3 properties

sourceKey: resource key

Alternate keys: list of temporary keys

fetcher: an instance of DataFetcher interface, which defines the loadData method to get data

DataFetcher interface: real resource acquisition

loadData(): get the data that can be decoded

cleanup(): empty or recycle resources

getDataClass(): the class of the resource that the current implementation class can obtain

getDataSource(): from which data source will this fetcher return data, enumeration type.

This article is composed of Ken opened. Release!

Published 16 original articles, won praise 2, visited 10000+
Private letter follow

Posted by opalelement on Mon, 17 Feb 2020 02:08:51 -0800