Source analysis of Android mainstream triplet libraries (3. Deep understanding of Glide source)

Keywords: Android OkHttp Retrofit Fragment

Preface

To be a great Android developer, you need a complete Knowledge System Here, let's grow up as we want.

tips: articles are too long to favor your collection first, then read them slowly ~

In the first two chapters, we have analyzed the core source code of OKHttp, the underlying framework of Android, and Retrofit, the encapsulation framework. If you are not familiar with the internal mechanism of OKHttp or Retrofit, you can see that Source analysis of Android mainstream triplet libraries (1. Deep understanding of OKHttp source) and Source analysis of Android mainstream triplet libraries (2. Deep understanding of Retrofit source) .In this article, we will take a closer look at the source loading process of Glide, the most widely used picture loading framework for Android.

1. Basic Use Process

The most basic flow of Glide usage is the following line of code, and all other extended extra functionality is added on top of its builder's chain call.

GlideApp.with(context).load(url).into(iv);

The GlideApp is automatically generated by the annotation processor. To use GlideApp, you must first configure the AppGlideModule module of the application, either empty or add the specified configuration as appropriate.

@GlideModule
public class MyAppGlideModule extends AppGlideModule {

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        // In actual use, the following configurations can be added as appropriate
        <!--builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));-->
        <!--int memoryCacheSizeBytes = 1024 * 1024 * 20;-->
        <!--builder.setMemoryCache(new LruResourceCache(memoryCacheSizeBytes));-->
        <!--int bitmapPoolSizeBytes = 1024 * 1024 * 30;-->
        <!--builder.setBitmapPool(new LruBitmapPool(bitmapPoolSizeBytes));-->
        <!--int diskCacheSizeBytes = 1024 * 1024 * 100;-->
        <!--builder.setDiskCache(new InternalCacheDiskCacheFactory(context, diskCacheSizeBytes));-->
    }
}

Next, this article will analyze and explain the process of Glide loading network pictures in detail for the latest source version of Glide V4.8.0, in order to let readers and friends know what it is.

2. GlideApp.with(context) Source Details

First of all, this Glide Frame Diagram, drawn from the Dream of Gentle Wild Dust, gives us a preliminary understanding of Glide's overall framework.

Starting with this line of GlideApp.with, the internal main line executes as follows.

1,GlideApp#with

return (GlideRequests) Glide.with(context);

2,Glide#with

return getRetriever(context).get(context);

return Glide.get(context).getRequestManagerRetriever();

// External synchronization with double-lock ensures only one Glide initialization at a time
checkAndInitializeGlide(context);

initializeGlide(context);

// Another overload method that eventually executes to Glide
initializeGlide(context, new GlideBuilder());

@SuppressWarnings("deprecation")
  private static void initializeGlide(@NonNull Context   context, @NonNull GlideBuilder builder) {
    Context applicationContext =     context.getApplicationContext();
    // 1. Get annotated GlideModule s from previous applications
    GeneratedAppGlideModule annotationGeneratedModule =     getAnnotationGeneratedGlideModules();
    // 2. If the GlideModule is empty or the flag inside the configurable manifest is true, get the inside of the manifest
    // Configured GlideModule modules (manifestModules).
    List<com.bumptech.glide.module.GlideModule>     manifestModules = Collections.emptyList();
    if (annotationGeneratedModule == null ||     annotationGeneratedModule.isManifestParsingEnabled(    )) {
      manifestModules = new   ManifestParser(applicationContext).parse();
    }

    ...

    RequestManagerRetriever.RequestManagerFactory     factory =
        annotationGeneratedModule != null
            ? annotationGeneratedModule.getRequestManag    erFactory() : null;
    builder.setRequestManagerFactory(factory);
    for (com.bumptech.glide.module.GlideModule module :     manifestModules) {
      module.applyOptions(applicationContext, builder);
    }
    if (annotationGeneratedModule != null) {
      annotationGeneratedModule.applyOptions(applicatio  nContext, builder);
    }
    // 3. Initialize various configuration information
    Glide glide = builder.build(applicationContext);
    // 4. Put the configuration information in manifestModules and annotation GeneratedModule in builder
    // Inside (applyOptions) replaces the glide default components (registerComponents)
    for (com.bumptech.glide.module.GlideModule module :     manifestModules) {
      module.registerComponents(applicationContext,   glide, glide.registry);
    }
    if (annotationGeneratedModule != null) {
      annotationGeneratedModule.registerComponents(appl  icationContext, glide, glide.registry);
    }
    applicationContext.registerComponentCallbacks(glide    );
    Glide.glide = glide;
}

3,GlideBuilder#build

@NonNull
  Glide build(@NonNull Context context) {
    // Create Request Picture Thread Pool sourceExecutor
    if (sourceExecutor == null) {
      sourceExecutor =   GlideExecutor.newSourceExecutor();
    }

    // Create a hard disk cache thread pool diskCacheExecutor
    if (diskCacheExecutor == null) {
      diskCacheExecutor =   GlideExecutor.newDiskCacheExecutor();
    }

    // Create Animation Thread Pool animationExecutor
    if (animationExecutor == null) {
      animationExecutor =   GlideExecutor.newAnimationExecutor();
    }

    if (memorySizeCalculator == null) {
      memorySizeCalculator = new   MemorySizeCalculator.Builder(context).build();
    }

    if (connectivityMonitorFactory == null) {
      connectivityMonitorFactory = new   DefaultConnectivityMonitorFactory();
    }

    if (bitmapPool == null) {
      // Set the size of various pool s according to the screen density and size of the device
      int size =   memorySizeCalculator.getBitmapPoolSize();
      if (size > 0) {
        // Create a Picture Thread Pool LruBitmapPool to cache all released bitmap s
        // Cache policy is SizeConfigStrategy when API is greater than 19 and less than AttributeStrategy.
        // SizeConfigStrategy is a HashMap with bitmap size and config as key and value as bitmap
        bitmapPool = new LruBitmapPool(size);
      } else {
        bitmapPool = new BitmapPoolAdapter();
      }
    }

    // Create object array cache pool LruArrayPool, default 4M
    if (arrayPool == null) {
      arrayPool = new   LruArrayPool(memorySizeCalculator.getArrayPoolSiz  eInBytes());
    }

    // Create LruResourceCache, Memory Cache
    if (memoryCache == null) {
      memoryCache = new   LruResourceCache(memorySizeCalculator.getMemoryCa  cheSize());
    }

    if (diskCacheFactory == null) {
      diskCacheFactory = new   InternalCacheDiskCacheFactory(context);
    }

    // Create task and resource management engines (thread pools, memory caches, and hard disk cache objects)
    if (engine == null) {
      engine =
          new Engine(
              memoryCache,
              diskCacheFactory,
              diskCacheExecutor,
              sourceExecutor,
              GlideExecutor.newUnlimitedSourceExecutor(  ),
              GlideExecutor.newAnimationExecutor(),
              isActiveResourceRetentionAllowed);
    }
    
    RequestManagerRetriever requestManagerRetriever =
    new RequestManagerRetriever(requestManagerFactory);

    return new Glide(
        context,
        engine,
        memoryCache,
        bitmapPool,
        arrayPool,
        requestManagerRetriever,
        connectivityMonitorFactory,
        logLevel,
        defaultRequestOptions.lock(),
        defaultTransitionOptions);
}

4. Glide#Glide construction method

Glide(...) {
    ...
    // Register a class for managing task execution objects (Registry)
    // Registry is a factory, and all registered objects are a factory employee, and when tasks are distributed,
    // Distribute to the appropriate employee for processing based on the nature of the current task
    registry = new Registry();
    
    ...
    
    // There are approximately 60 append or register employee components (parser, codec, factory class, transcoder class, etc.)
    registry
    .append(ByteBuffer.class, new ByteBufferEncoder())
    .append(InputStream.class, new StreamEncoder(arrayPool))
    
    // Output target of corresponding type (BitmapImageViewTarget / DrawableImageViewTarget) based on a given subclass
    ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory();
    
    glideContext =
        new GlideContext(
            context,
            arrayPool,
            registry,
            imageViewTargetFactory,
            defaultRequestOptions,
            defaultTransitionOptions,
            engine,
            logLevel);
}

5,RequestManagerRetriever#get

@NonNull
public RequestManager get(@NonNull Context context) {
  if (context == null) {
    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, take the appropriate get overload method
    if (context instanceof FragmentActivity) {
      return get((FragmentActivity) context);
    } else if (context instanceof Activity) {
      return get((Activity) context);
    } else if (context instanceof ContextWrapper) {
      return get(((ContextWrapper) context).getBaseContext());
    }
  }

  // Otherwise, associate the request directly with the Application Lifecycle
  return getApplicationManager(context);
}

This concludes that if the current incoming context is an application or the current thread is a child thread, the life cycle of the request is associated with the ApplicationLifecycle; otherwise, if the context is a FragmentActivity or Fragment, when the current component adds a SupportFragment (SupportRequestManagerFragment), when the context is an Activity, when the current component adds a Fragment (RequestM Fragment)AnagerFragment).

6. GlideApp#with Summary

1. Initialize a wide variety of configuration information (including cache, request thread pool, size, picture format, etc.) and glide objects.
2. Bind the glide request with the life cycle of application/SupportFragment/Fragment.
Here we review the implementation process of the with method.

3. Detailed source code for load(url)

1,GlideRequest(RequestManager)#load

return (GlideRequest<Drawable>) super.load(string);

return asDrawable().load(string);

// 1. asDrawable section
return (GlideRequest<Drawable>) super.asDrawable();

return as(Drawable.class);

// A GlideRequest (a subclass of RequestManager) was eventually returned
return new GlideRequest<>(glide, this, resourceClass, context);

// 2. load section
return (GlideRequest<TranscodeType>) super.load(string);

return loadGeneric(string);

@NonNull
private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {
    // model is the set url
    this.model = model;
    // Record url set
    isModelSet = true;
    return this;
}

You can see that the source code for this part of load is very simple: it sets the mode (url) to be requested for GlideRequest (RequestManager) and records the status of the URL that has been set.

Here, let's take a look at the implementation of the load method.

4. In (iv) Source Details

The early warning starts where the real complexity is.

1,RequestBuilder.into

 @NonNull
public ViewTarget<ImageView, TranscodeType>   into(@NonNull ImageView view) {
  Util.assertMainThread();
  Preconditions.checkNotNull(view);

  RequestOptions requestOptions =     this.requestOptions;
  if (!requestOptions.isTransformationSet()
      && requestOptions.isTransformationAllowed()
      && view.getScaleType() != null) {
    // Clone in this method so that if we use this   RequestBuilder to load into a View and then
    // into a different target, we don't retain the   transformation applied based on the previous
    // View's scale type.
    switch (view.getScaleType()) {
      // This RequestOptions holds the scaleType to be set, and Glide encapsulates CenterCrop, CenterInside, and
      // FitCenter, CenterInside four specifications.
      case CENTER_CROP:
        requestOptions =   requestOptions.clone().optionalCenterCrop();
        break;
      case CENTER_INSIDE:
        requestOptions =   requestOptions.clone().optionalCenterInside()  ;
        break;
      case FIT_CENTER:
      case FIT_START:
      case FIT_END:
        requestOptions =   requestOptions.clone().optionalFitCenter();
        break;
      case FIT_XY:
        requestOptions =   requestOptions.clone().optionalCenterInside()  ;
        break;
      case CENTER:
      case MATRIX:
      default:
        // Do nothing.
    }
  }

  // Note that this transcodeClass refers to drawable or bitmap
  return into(
      glideContext.buildImageViewTarget(view,     transcodeClass),
      /*targetListener=*/ null,
      requestOptions);
}

2,GlideContext#buildImageViewTarget

return imageViewTargetFactory.buildTarget(imageView, transcodeClass);

3,ImageViewTargetFactory#buildTarget

@NonNull
@SuppressWarnings("unchecked")
public <Z> ViewTarget<ImageView, Z>   buildTarget(@NonNull ImageView view,
    @NonNull Class<Z> clazz) {
  // Returns the target object displaying the Bimtap/Drawable resource
  if (Bitmap.class.equals(clazz)) {
    return (ViewTarget<ImageView, Z>) new   BitmapImageViewTarget(view);
  } else if (Drawable.class.isAssignableFrom(clazz))     {
    return (ViewTarget<ImageView, Z>) new   DrawableImageViewTarget(view);
  } else {
    throw new IllegalArgumentException(
        "Unhandled class: " + clazz + ", try   .as*(Class).transcode(ResourceTranscoder)");
  }
}

As you can see, there are only two types of target s maintained inside Glide, one is BitmapImageViewTarget, the other is DrawableImageViewTarget, and you'll move on.

4,RequestBuilder#into

private <Y extends Target<TranscodeType>> Y into(
      @NonNull Y target,
      @Nullable RequestListener<TranscodeType>   targetListener,
      @NonNull RequestOptions options) {
    Util.assertMainThread();
    Preconditions.checkNotNull(target);
    if (!isModelSet) {
      throw new IllegalArgumentException("You must call   #load() before calling #into()");
    }

    options = options.autoClone();
    // Analysis 1. Create Request
    Request request = buildRequest(target,     targetListener, options);

    Request previous = target.getRequest();
    if (request.isEquivalentTo(previous)
        && !isSkipMemoryCacheWithCompletePreviousReques    t(options, previous)) {
      request.recycle();
      // If the request is completed, beginning again   will ensure the result is re-delivered,
      // triggering RequestListeners and Targets. If   the request is failed, beginning again will
      // restart the request, giving it another chance   to complete. If the request is already
      // running, we can let it continue running   without interruption.
      if (!Preconditions.checkNotNull(previous).isRunni  ng()) {
        // Use the previous request rather than the new     one to allow for optimizations like skipping
        // setting placeholders, tracking and     un-tracking Targets, and obtaining View     dimensions
        // that are done in the individual Request.
        previous.begin();
      }
      return target;
    }
    
    requestManager.clear(target);
    target.setRequest(request);
    // Analysis 2. Really Track Where Requests Are
    requestManager.track(target, request);

    return target;
}

// Analysis 1
private Request buildRequest(
      Target<TranscodeType> target,
      @Nullable RequestListener<TranscodeType>   targetListener,
      RequestOptions requestOptions) {
    return buildRequestRecursive(
        target,
        targetListener,
        /*parentCoordinator=*/ null,
        transitionOptions,
        requestOptions.getPriority(),
        requestOptions.getOverrideWidth(),
        requestOptions.getOverrideHeight(),
        requestOptions);
}

// Analysis 1
private Request buildRequestRecursive(
      Target<TranscodeType> target,
      @Nullable RequestListener<TranscodeType>   targetListener,
      @Nullable RequestCoordinator parentCoordinator,
      TransitionOptions<?, ? super TranscodeType>   transitionOptions,
      Priority priority,
      int overrideWidth,
      int overrideHeight,
      RequestOptions requestOptions) {

    // Build the ErrorRequestCoordinator first if     necessary so we can update parentCoordinator.
    ErrorRequestCoordinator errorRequestCoordinator =     null;
    if (errorBuilder != null) {
      // Create errorRequestCoordinator (exception handling object)
      errorRequestCoordinator = new   ErrorRequestCoordinator(parentCoordinator);
      parentCoordinator = errorRequestCoordinator;
    }

    // Recursive Thumbnail Creation Request
    Request mainRequest =
        buildThumbnailRequestRecursive(
            target,
            targetListener,
            parentCoordinator,
            transitionOptions,
            priority,
            overrideWidth,
            overrideHeight,
            requestOptions);

    if (errorRequestCoordinator == null) {
      return mainRequest;
    }

    ...
    
    Request errorRequest =     errorBuilder.buildRequestRecursive(
        target,
        targetListener,
        errorRequestCoordinator,
        errorBuilder.transitionOptions,
        errorBuilder.requestOptions.getPriority(),
        errorOverrideWidth,
        errorOverrideHeight,
        errorBuilder.requestOptions);
    errorRequestCoordinator.setRequests(mainRequest,     errorRequest);
    return errorRequestCoordinator;
}

// Analysis 1
private Request buildThumbnailRequestRecursive(
      Target<TranscodeType> target,
      RequestListener<TranscodeType> targetListener,
      @Nullable RequestCoordinator parentCoordinator,
      TransitionOptions<?, ? super TranscodeType> transitionOptions,
      Priority priority,
      int overrideWidth,
      int overrideHeight,
      RequestOptions requestOptions) {
    if (thumbnailBuilder != null) {
      // Recursive case: contains a potentially recursive thumbnail request builder.
      
      ...

      ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
      // Get a normal request object
      Request fullRequest =
          obtainRequest(
              target,
              targetListener,
              requestOptions,
              coordinator,
              transitionOptions,
              priority,
              overrideWidth,
              overrideHeight);
      isThumbnailBuilt = true;
      // Recursively generate thumbnail requests.
      // Create a thumbnail request object recursively
      Request thumbRequest =
          thumbnailBuilder.buildRequestRecursive(
              target,
              targetListener,
              coordinator,
              thumbTransitionOptions,
              thumbPriority,
              thumbOverrideWidth,
              thumbOverrideHeight,
              thumbnailBuilder.requestOptions);
      isThumbnailBuilt = false;
      // The coordinator (ThumbnailRequestCoordinator) acts as the coordinator for both.
      // Requests that can load both thumbnails and normal graphs
      coordinator.setRequests(fullRequest, thumbRequest);
      return coordinator;
    } else if (thumbSizeMultiplier != null) {
      // Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse.
      // When thumbSizeMultiplier (0 ~ 1) is set as the ratio of thumb,
      // Thumbnail request does not need to be built recursively
      ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
      Request fullRequest =
          obtainRequest(
              target,
              targetListener,
              requestOptions,
              coordinator,
              transitionOptions,
              priority,
              overrideWidth,
              overrideHeight);
      RequestOptions thumbnailOptions = requestOptions.clone()
          .sizeMultiplier(thumbSizeMultiplier);

      Request thumbnailRequest =
          obtainRequest(
              target,
              targetListener,
              thumbnailOptions,
              coordinator,
              transitionOptions,
              getThumbnailPriority(priority),
              overrideWidth,
              overrideHeight);

      coordinator.setRequests(fullRequest, thumbnailRequest);
      return coordinator;
    } else {
      // Base case: no thumbnail.
      // Get a normal map request directly when there is no thumbnail request
      return obtainRequest(
          target,
          targetListener,
          requestOptions,
          parentCoordinator,
          transitionOptions,
          priority,
          overrideWidth,
          overrideHeight);
    }
}

private Request obtainRequest(
      Target<TranscodeType> target,
      RequestListener<TranscodeType> targetListener,
      RequestOptions requestOptions,
      RequestCoordinator requestCoordinator,
      TransitionOptions<?, ? super TranscodeType>   transitionOptions,
      Priority priority,
      int overrideWidth,
      int overrideHeight) {
    // The final actual return is a SingleRequest object that loads the specified resource into the corresponding Target
    return SingleRequest.obtain(
        context,
        glideContext,
        model,
        transcodeClass,
        requestOptions,
        overrideWidth,
        overrideHeight,
        priority,
        target,
        targetListener,
        requestListeners,
        requestCoordinator,
        glideContext.getEngine(),
        transitionOptions.getTransitionFactory());
}

From the source analysis above, we set up a request in the buildRequest() method at analysis 1, and at most we can request thumbnails and normal maps simultaneously. Finally, we call the requestManager.track(target, request) method and see what's going on inside the trace.

5,RequestManager#track

// Analysis 2
void track(@NonNull Target<?> target, @NonNull Request request) {
    // Join a target target collection (Set)
    targetTracker.track(target);
    
    requestTracker.runRequest(request);
}

6,RequestTracker#runRequest

/**
* Starts tracking the given request.
*/
// Analysis 2
public void runRequest(@NonNull Request request) {
    requests.add(request);
    if (!isPaused) {
      // Start the request if it is not paused
      request.begin();
    } else {
      request.clear();
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Paused, delaying request");
      }
      // Otherwise, empty the requests and join the delayed request queue (ArrayList implementation is used to maintain a strong reference to these requests)
      pendingRequests.add(request);
    }
}

7,SingleRequest#begin

// Analysis 2
@Override
public void begin() {
  
  ...
  
  if (model == null) {
  
    ...
    // model (url) is empty, callback loading failed
    onLoadFailed(new GlideException("Received null   model"), logLevel);
    return;
  }

  if (status == Status.RUNNING) {
    throw new IllegalArgumentException("Cannot   restart a running request");
  }

 
  if (status == Status.COMPLETE) {
    onResourceReady(resource,   DataSource.MEMORY_CACHE);
    return;
  }

  status = Status.WAITING_FOR_SIZE;
  if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
    // When the override() API is used to specify a fixed width and height for the picture, onSizeReady is executed directly.
    // The final core processing is on SizeReady
    onSizeReady(overrideWidth, overrideHeight);
  } else {
    // Calculate the width and height of the image based on the width and height of the imageView, and you will eventually reach onSizeReady
    target.getSize(this);
  }

  if ((status == Status.RUNNING || status ==     Status.WAITING_FOR_SIZE)
      && canNotifyStatusChanged()) {
    // Preload set thumbnails
    target.onLoadStarted(getPlaceholderDrawable());
  }
  if (IS_VERBOSE_LOGGABLE) {
    logV("finished run method in " +   LogTime.getElapsedMillis(startTime));
  }
}

From requestManager.track(target, request) onSizeReady, which will eventually execute to the SingleRequest#begin() method, you can guess (since only preloaded thumbnails are processed later). The real request starts here. Let's explore ~

8,SingleRequest#onSizeReady

// Analysis 2
@Override
public void onSizeReady(int width, int height) {
  stateVerifier.throwIfRecycled();
  
  ...
  
  status = Status.RUNNING;

  float sizeMultiplier =     requestOptions.getSizeMultiplier();
  this.width = maybeApplySizeMultiplier(width,     sizeMultiplier);
  this.height = maybeApplySizeMultiplier(height,     sizeMultiplier);

  ...
  
  // Engine is an engine class responsible for loading, managing active and cached resources based on a given configuration
  loadStatus = engine.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.getUseUnlimitedSourceGeneratorsP    ool(),
      requestOptions.getUseAnimationPool(),
      requestOptions.getOnlyRetrieveFromCache(),
      this);

  ...
}

Finally I see the Engine class, I feel it's not far from success, continue ~

9,Engine#load

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) {
  
  ...

  // Find from weak references first, call back onResourceReady if there is one, and return directly
  EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
  if (active != null) {
    cb.onResourceReady(active,   DataSource.MEMORY_CACHE);
    if (VERBOSE_IS_LOGGABLE) {
      logWithTimeAndKey("Loaded resource from active     resources", startTime, key);
    }
    return null;
  }

  // No more lookups from memory, or they will be taken out and put into ActiveResources (weakly referenced cached map maintained internally)
  EngineResource<?> cached = loadFromCache(key,     isMemoryCacheable);
  if (cached != null) {
    cb.onResourceReady(cached,   DataSource.MEMORY_CACHE);
    if (VERBOSE_IS_LOGGABLE) {
      logWithTimeAndKey("Loaded resource from cache",     startTime, key);
    }
    return null;
  }

  EngineJob<?> current = jobs.get(key,     onlyRetrieveFromCache);
  if (current != null) {
    current.addCallback(cb);
    if (VERBOSE_IS_LOGGABLE) {
      logWithTimeAndKey("Added to existing load",     startTime, key);
    }
    return new LoadStatus(cb, current);
  }

  // If not in memory, create engineJob (decodejob callback class, manage download process and status)
  EngineJob<R> engineJob =
      engineJobFactory.build(
          key,
          isMemoryCacheable,
          useUnlimitedSourceExecutorPool,
          useAnimationPool,
          onlyRetrieveFromCache);

  // Create Parsing Work Object
  DecodeJob<R> decodeJob =
      decodeJobFactory.build(
          glideContext,
          model,
          key,
          signature,
          width,
          height,
          resourceClass,
          transcodeClass,
          priority,
          diskCacheStrategy,
          transformations,
          isTransformationRequired,
          isScaleOnlyOrNoTransform,
          onlyRetrieveFromCache,
          options,
          engineJob);

  // Place in a HashMap maintained inside Jobs
  jobs.put(key, engineJob);

  // Focus 8 will be used for post-analysis
  // Register ResourceCallback interface
  engineJob.addCallback(cb);
  // Internal open thread to request
  engineJob.start(decodeJob);

  if (VERBOSE_IS_LOGGABLE) {
    logWithTimeAndKey("Started new load", startTime,   key);
  }
  return new LoadStatus(cb, engineJob);
}

public void start(DecodeJob<R> decodeJob) {
    this.decodeJob = decodeJob;
    // The willDecodeFromCache method returns true internally according to different stage stages, if it is RESOURCE_CACHE/DATA_CACHE, and diskCacheExecutor otherwise calls getActiveSourceExecutor, which returns sourceUnlimitedExecutor/animationExecutor/sourceExecutor internally according to the corresponding conditions
    GlideExecutor executor =   
    decodeJob.willDecodeFromCache()
        ? diskCacheExecutor
        : getActiveSourceExecutor();
    executor.execute(decodeJob);
}

You can see that the final Engine class executes its own start method, which uses diskCacheExecutor/sourceUnlimitedExecutor/animationExecutor/sourceExecutor to perform the final decodeJob decoding task, depending on the configuration.

10,DecodeJob#run

runWrapped();

private void runWrapped() {
    switch (runReason) {
      case INITIALIZE:
        stage = getNextStage(Stage.INITIALIZE);
        // Focus 1
        currentGenerator = getNextGenerator();
        // startNext() of the corresponding Generator is called inside Focus 2
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        // Focus 3 decodes the acquired data into corresponding resources
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized     run reason: " + runReason);
    }
}

// Focus 1, where ResourceCacheGenerator, DataCacheGenerator, and SourcceGenerator objects are generated asynchronously in turn, and startNext() is executed afterwards
private DataFetcherGenerator getNextGenerator() {
    switch (stage) {
      case RESOURCE_CACHE:
        return new ResourceCacheGenerator(decodeHelper, this);
      case DATA_CACHE:
        return new DataCacheGenerator(decodeHelper, this);
      case SOURCE:
        return new SourceGenerator(decodeHelper, this);
      case FINISHED:
        return null;
      default:
        throw new IllegalStateException("Unrecognized     stage: " + stage);
    }
}

11,SourceGenerator#startNext

// Focus 2
@Override
public boolean startNext() {
  // Cache data to hard disk if it is not empty (the first execution of this method will not be called)
  if (dataToCache != null) {
    Object data = dataToCache;
    dataToCache = null;
    cacheData(data);
  }

  if (sourceCacheGenerator != null &&     sourceCacheGenerator.startNext()) {
    return true;
  }
  sourceCacheGenerator = null;

  loadData = null;
  boolean started = false;
  while (!started && hasNextModelLoader()) {
    // The ModelLoader object is found inside the focus 4 getLoadData() method in modelLoaders
    // (Each Generator corresponds to a ModelLoader),
    // And return a loadData list using the modelLoader.buildLoadData method
    loadData =   helper.getLoadData().get(loadDataListIndex++);
    if (loadData != null
        && (helper.getDiskCacheStrategy().isDataCache  able(loadData.fetcher.getDataSource())
        || helper.hasLoadPath(loadData.fetcher.getDat  aClass()))) {
      started = true;
      // Focus 6 uses the fetcher object of the loadData object (an analysis of Focus 3 shows that its implementation class is HttpUrlFetcher)
      // loadData method to get picture data
      loadData.fetcher.loadData(helper.getPriority(),     this);
    }
  }
  return started;
}

12,DecodeHelper#getLoadData

List<LoadData<?>> getLoadData() {
    if (!isLoadDataSet) {
      isLoadDataSet = true;
      loadData.clear();
      List<ModelLoader<Object, ?>> modelLoaders =   glideContext.getRegistry().getModelLoaders(model)  ;
      //noinspection ForLoopReplaceableByForEach to   improve perf
      for (int i = 0, size = modelLoaders.size(); i <   size; i++) {
        ModelLoader<Object, ?> modelLoader =     modelLoaders.get(i);
        // Note: The actual loadData object is ultimately obtained here from the buildLoadData of HttpGlideUrlLoader
        LoadData<?> current =
            modelLoader.buildLoadData(model, width,     height, options);
        if (current != null) {
          loadData.add(current);
        }
      }
    }
    return loadData;
}

13,HttpGlideUrlLoader#buildLoadData

@Override
public LoadData<InputStream> buildLoadData(@NonNull   GlideUrl model, int width, int height,
    @NonNull Options options) {
  // GlideUrls memoize parsed URLs so caching them     saves a few object instantiations and time
  // spent parsing urls.
  GlideUrl url = model;
  if (modelCache != null) {
    url = modelCache.get(model, 0, 0);
    if (url == null) {
      // Focus 5
      modelCache.put(model, 0, 0, model);
      url = model;
    }
  }
  int timeout = options.get(TIMEOUT);
  // Notice that an implementation class HttpUrlFetcher for DataFetcher is created here
  return new LoadData<>(url, new HttpUrlFetcher(url,     timeout));
}

// Focus 5
public void put(A model, int width, int height, B value) {
    ModelKey<A> key = ModelKey.get(model, width,     height);
    // Finally, the corresponding value is cached through LruCache, and key is a ModelKey object (composed of model, width, height attributes)
    cache.put(key, value);
}

From this analysis, we understand that HttpUrlFetcher is actually the ultimate requester, and that Glide uses LruCache to cache the parsed url so that subsequent url resolution time can be omitted.

14,HttpUrlFetcher#loadData

@Override
public void loadData(@NonNull Priority priority,
    @NonNull DataCallback<? super InputStream>   callback) {
  long startTime = LogTime.getLogTime();
  try {
    // Focus 6
    // Internally, loadDataWithRedirects requests data through the HttpURLConnection network
    InputStream result =   loadDataWithRedirects(glideUrl.toURL(), 0, null,   glideUrl.getHeaders());
    // The request successfully callback onDataReady()
    callback.onDataReady(result);
  } catch (IOException e) {
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(TAG, "Failed to load data for url", e);
    }
    callback.onLoadFailed(e);
  } finally {
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      Log.v(TAG, "Finished http url fetcher fetch in     " + LogTime.getElapsedMillis(startTime));
    }
  }
}

private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl,
  Map<String, String> headers) throws IOException {
    
    ...

    urlConnection.connect();
    // Set the stream so that it's closed in cleanup to avoid resource leaks. See #2352.
    stream = urlConnection.getInputStream();
    if (isCancelled) {
      return null;
    }
    final int statusCode = urlConnection.getResponseCode();
    // Success is judged as long as the status code is in the form of 2xx
    if (isHttpOk(statusCode)) {
      // Get resource flow from urlConnection
      return getStreamForSuccessfulRequest(urlConnection);
    } else if (isHttpRedirect(statusCode)) {
    
      ...
      
      // Redirecting Requests
      return loadDataWithRedirects(redirectUrl, redirects + 1, url,   headers);
    } else if (statusCode == INVALID_STATUS_CODE) {
      throw new HttpException(statusCode);
    } else {
      throw new HttpException(urlConnection.getResponseMessage(),   statusCode);
    }
}

private InputStream getStreamForSuccessfulRequest(HttpURLConnection urlConnection)
  throws IOException {
    if (TextUtils.isEmpty(urlConnection.getContentEncoding())) {
      int contentLength = urlConnection.getContentLength();
      stream = ContentLengthInputStream.obtain(urlConnection.getInputStr  eam(), contentLength);
    } else {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Got non empty content encoding: " +     urlConnection.getContentEncoding());
      }
      stream = urlConnection.getInputStream();
    }
    return stream;
}

In the loadDataWithRedirects of the HttpUrlFetcher#loadData method, Glide makes a request through the native HttpURLConnection and calls the getStreamForSuccessfulRequest() method to get the final image stream.

15,DecodeJob#run

After we request the corresponding stream through the loadData() method of HtttpUrlFetcher, we must also process the stream to get the resources we want ultimately.Here we go back to point of interest 3 of the DecodeJob#run method in step 10, which will decode the stream.

decodeFromRetrievedData();

Next, continue to look at his internal processes.

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 {
      //  Core Code 
      // Decode resources from data
      resource = decodeFromData(currentFetcher, currentData,   currentDataSource);
    } catch (GlideException e) {
      e.setLoggingDetails(currentAttemptingKey, currentDataSource);
      throwables.add(e);
    }
    if (resource != null) {
      // Focus 8 
      // Encoding and publishing the resulting Resource<Bitmap>object
      notifyEncodeAndRelease(resource, currentDataSource);
    } else {
      runGenerators();
    }
}

 private <Data> Resource<R> decodeFromData(DataFetcher<?> fetcher, Data data,
  DataSource dataSource) throws GlideException {
    try {
      if (data == null) {
        return null;
      }
      long startTime = LogTime.getLogTime();
      // Core Code
      // Further Packaging Undercoding Method
      Resource<R> result = decodeFromFetcher(data, dataSource);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Decoded result " + result, startTime);
      }
      return result;
    } finally {
      fetcher.cleanup();
    }
}

@SuppressWarnings("unchecked")
private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource)
  throws GlideException {
    LoadPath<Data, ?, R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());
    // Core Code
    // Distribute decoding tasks to LoadPath
    return runLoadPath(data, dataSource, path);
}

private <Data, ResourceType> Resource<R> runLoadPath(Data data, DataSource dataSource,
  LoadPath<Data, ResourceType, R> path) throws GlideException {
    Options options = getOptionsWithHardwareConfig(dataSource);
    // Further wrapping data
    DataRewinder<Data> rewinder =     glideContext.getRegistry().getRewinder(data);
    try {
      // ResourceType in DecodeCallback below is required for   compilation to work with gradle.
      // Core Code
      // Distribute decoding tasks to LoadPath
      return path.load(
          rewinder, options, width, height, new   DecodeCallback<ResourceType>(dataSource));
    } finally {
      rewinder.cleanup();
    }
}

16,LoadPath#load

public Resource<Transcode> load(DataRewinder<Data> rewinder, @NonNull Options options, int width,
  int height, DecodePath.DecodeCallback<ResourceType> decodeCallback) throws GlideException {
List<Throwable> throwables = Preconditions.checkNotNull(listPool.acquire());
try {
  // Core Code
  return loadWithExceptionList(rewinder, options, width, height, decodeCallback, throwables);
} finally {
  listPool.release(throwables);
}

}

private Resource<Transcode> loadWithExceptionList(DataRewinder<Data> rewinder,
      @NonNull Options options,
      int width, int height, DecodePath.DecodeCallback<ResourceType>   decodeCallback,
      List<Throwable> exceptions) throws GlideException {
    Resource<Transcode> result = null;
    //noinspection ForLoopReplaceableByForEach to improve perf
    for (int i = 0, size = decodePaths.size(); i < size; i++) {
      DecodePath<Data, ResourceType, Transcode> path =   decodePaths.get(i);
      try {
        // Core Code
        // Decode method to further distribute decoding tasks to DecodePath to decode
        result = path.decode(rewinder, width, height, options,     decodeCallback);
      } catch (GlideException e) {
        exceptions.add(e);
      }
      if (result != null) {
        break;
      }
    }

    if (result == null) {
      throw new GlideException(failureMessage, new   ArrayList<>(exceptions));
    }

    return result;
}

17,DecodePath#decode

public Resource<Transcode> decode(DataRewinder<DataType> rewinder,     int width, int height,
      @NonNull Options options, DecodeCallback<ResourceType> callback)   throws GlideException {
    // Core Code
    // Continue to call DecodePath's decodeResource method to parse out the data
    Resource<ResourceType> decoded = decodeResource(rewinder, width,     height, options);
    Resource<ResourceType> transformed =     callback.onResourceDecoded(decoded);
    return transcoder.transcode(transformed, options);
}

@NonNull
private Resource<ResourceType> decodeResource(DataRewinder<DataType>   rewinder, int width,
    int height, @NonNull Options options) throws GlideException {
  List<Throwable> exceptions =     Preconditions.checkNotNull(listPool.acquire());
  try {
    // Core Code
    return decodeResourceWithList(rewinder, width, height, options,   exceptions);
  } finally {
    listPool.release(exceptions);
  }
}

@NonNull
private Resource<ResourceType>   decodeResourceWithList(DataRewinder<DataType> rewinder, int width,
    int height, @NonNull Options options, List<Throwable> exceptions)   throws GlideException {
  Resource<ResourceType> result = null;
  //noinspection ForLoopReplaceableByForEach to improve perf
  for (int i = 0, size = decoders.size(); i < size; i++) {
    ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
    try {
      DataType data = rewinder.rewindAndGet();
      if (decoder.handles(data, options)) {
        // Get packaged data
        data = rewinder.rewindAndGet();
        // Core Code 
        // Distribute to different decoders according to the type of DataType and ResourceType
        result = decoder.decode(data, width, height, options);
      }
    } catch (IOException | RuntimeException | OutOfMemoryError e) {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Failed to decode data for " + decoder, e);
      }
      exceptions.add(e);
    }

    if (result != null) {
      break;
    }
  }

  if (result == null) {
    throw new GlideException(failureMessage, new   ArrayList<>(exceptions));
  }
  return result;
}

You can see that after a series of nested calls, decoder.decode() is finally executed. decode is a ResourceDecoder < DataType, ResourceType > interface (resource decoder). It has different implementation classes depending on the different DataType and ResourceType, where the implementation class is ByteBufferBitmapDecoder. Let's take a look inside this decoder.Decoding process.

18,ByteBufferBitmapDecoder#decode

/**
 * Decodes {@link android.graphics.Bitmap Bitmaps} from {@link    java.nio.ByteBuffer ByteBuffers}.
 */
public class ByteBufferBitmapDecoder implements     ResourceDecoder<ByteBuffer, Bitmap> {
  
  ...

  @Override
  public Resource<Bitmap> decode(@NonNull ByteBuffer source, int width,   int height,
      @NonNull Options options)
      throws IOException {
    InputStream is = ByteBufferUtil.toStream(source);
    // Core Code
    return downsampler.decode(is, width, height, options);
  }
}

You can see that the end result is a downsampler, which is a compressor, mainly for stream decoding, compression, rounding, and so on.

19,DownSampler#decode

public Resource<Bitmap> decode(InputStream is, int outWidth, int outHeight,
  Options options) throws IOException {
    return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS);
}

 @SuppressWarnings({"resource", "deprecation"})
public Resource<Bitmap> decode(InputStream is, int requestedWidth, int requestedHeight,
      Options options, DecodeCallbacks callbacks) throws IOException {
    Preconditions.checkArgument(is.markSupported(), "You must provide an     InputStream that supports"
        + " mark()");

    ...

    try {
      // Core Code
      Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions,
          downsampleStrategy, decodeFormat, isHardwareConfigAllowed,   requestedWidth,
          requestedHeight, fixBitmapToRequestedDimensions, callbacks);
      // Focus 7   
      // Once the Bitmap object is decoded, it is wrapped as a BitmapResource object and returned.
      // Get the Resource <Bitmap>object through the internal get method
      return BitmapResource.obtain(result, bitmapPool);
    } finally {
      releaseOptions(bitmapFactoryOptions);
      byteArrayPool.put(bytesForOptions);
    }
}

private Bitmap decodeFromWrappedStreams(InputStream is,
      BitmapFactory.Options options, DownsampleStrategy downsampleStrategy,
      DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, int requestedWidth,
      int requestedHeight, boolean fixBitmapToRequestedDimensions,
      DecodeCallbacks callbacks) throws IOException {
    
    // Eliminate a series of non-core logic such as calculating the compression ratio
    ...
    
    // Core Code
    Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
    callbacks.onDecodeComplete(bitmapPool, downsampled);

    ...

    // Bimtap Rotation Processing
    ...
    
    return rotated;
}

private static Bitmap decodeStream(InputStream is,     BitmapFactory.Options options,
      DecodeCallbacks callbacks, BitmapPool bitmapPool) throws   IOException {
    
    ...
    
    TransformationUtils.getBitmapDrawableLock().lock();
    try {
      // Core Code
      result = BitmapFactory.decodeStream(is, null, options);
    } catch (IllegalArgumentException e) {
      ...
    } finally {
      TransformationUtils.getBitmapDrawableLock().unlock();
    }

    if (options.inJustDecodeBounds) {
      is.reset();
    }
    return result;
}

From the source flow above, we know that the last step is to use BitmapFactory.decodeStream() in the decodeStream() method of DownSampler to get Bitmap objects.Then, let's go back to the DownSampler#decode method in step 19 and see Focus 7. This is where Bitmap is wrapped as a BitmapResource object and returned, where the Resource object can be obtained by the internal get method, and then back to the DecodeJob#run method in step 15, where the Resource object is sent using the notifyEncodeAndRelease() methodCloth.

20,DecodeJob#notifyEncodeAndRelease

private void notifyEncodeAndRelease(Resource<R> resource, DataSource     dataSource) {
 
    ...

    notifyComplete(result, dataSource);

    ...
    
}

private void notifyComplete(Resource<R> resource, DataSource     dataSource) {
    setNotifiedOrThrow();
    callback.onResourceReady(resource, dataSource);
}

From the source code of EngineJob above, it implements the interface DecodeJob.CallBack.

class EngineJob<R> implements DecodeJob.Callback<R>,
    Poolable {
    ...
}

21,EngineJob#onResourceReady

@Override
public void onResourceReady(Resource<R> resource, DataSource   dataSource) {
  this.resource = resource;
  this.dataSource = dataSource;
  MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
}

private static class MainThreadCallback implements Handler.Callback{

    ...

    @Override
    public boolean handleMessage(Message message) {
      EngineJob<?> job = (EngineJob<?>) message.obj;
      switch (message.what) {
        case MSG_COMPLETE:
          // Core Code
          job.handleResultOnMainThread();
          break;
        ...
      }
      return true;
    }
}

From the source code above, the thread is switched through the main thread Handler object, and the handleResultOnMainThread method is called on the main thread.

@Synthetic
void handleResultOnMainThread() {
  ...

  //noinspection ForLoopReplaceableByForEach to improve perf
  for (int i = 0, size = cbs.size(); i < size; i++) {
    ResourceCallback cb = cbs.get(i);
    if (!isInIgnoredCallbacks(cb)) {
      engineResource.acquire();
      cb.onResourceReady(engineResource, dataSource);
    }
  }
 
  ...
}

Here we call all the ResourceCallback methods through a loop, and let's go back to the line of focus 8 of the Engine#load method at step 9. The ResourceCallback is registered here. In the engine.load of the SingleRequest#onSizeReady method at step 8, we see the last parameter, which is passed in. You can see that engineJob.addCallback(cb) is hereThe implementation class for CB of is SingleRequest.Next, let's look at the onResourceReady method of SingleRequest.

22,SingleRequest#onResourceReady

/**
 * A callback method that should never be invoked directly.
 */
@SuppressWarnings("unchecked")
@Override
public void onResourceReady(Resource<?> resource, DataSource   dataSource) {
  ...
  
  // Get Bitmap objects from Resource <Bitmap>
  Object received = resource.get();
  
  ...
  
  onResourceReady((Resource<R>) resource, (R) received, dataSource);
}

private void onResourceReady(Resource<R> resource, R resultDataSource dataSource) {

    ...

    try {
      ...

      if (!anyListenerHandledUpdatingTarget) {
        Transition<? super R> animation =
            animationFactory.build(dataSource, isFirstResource);
        // Core Code
        target.onResourceReady(result, animation);
      }
    } finally {
      isCallingCallbacks = false;
    }

    notifyLoadSuccess();
}

The target.onResourceReady(result, animation) method is also called in the SingleRequest#onResourceReady method, where the target is actually the BitmapImageViewTarget that we built in the into method. When we see the ImageViewTarget class, we don't find the onResourceReady method, but we find the onResourceReady method from its subclass ImageViewTarget, and from there we find the onResourceReady methodHere we go on looking down.

23,ImageViewTarget#onResourceReady

public abstract class ImageViewTarget<Z> extends ViewTarget<ImageView, Z>
implements Transition.ViewAdapter {

    ...

    @Override
    public void onResourceReady(@NonNull Z resource, @Nullable       Transition<? super Z> transition) {
      if (transition == null || !transition.transition(resource, this))   {
        // Core Code
        setResourceInternal(resource);
      } else {
        maybeUpdateAnimatable(resource);
      }
    }
 
    ...
    
    private void setResourceInternal(@Nullable Z resource) {
        // Order matters here. Set the resource first to make sure that the         Drawable has a valid and
        // non-null Callback before starting it.
        // Core Code
        setResource(resource);
        maybeUpdateAnimatable(resource);
    }
    
    // Core Code
    protected abstract void setResource(@Nullable Z resource);
}

Here we go back to the setResource method of the BitmapImageViewTarget and we finally see that Bitmap is set on the current imageView.

public class BitmapImageViewTarget extends ImageViewTarget<Bitmap> {

    ...
    
    
    @Override
    protected void setResource(Bitmap resource) {
      view.setImageBitmap(resource);
    }
}

At this point, our analysis is over. From the above analysis, Glide put most of the logical processing in the last into method, which took more than 20 analysis steps to request a stream of pictures, decode them, and finally set them on the corresponding imageView.

Finally, here's a complete Glide loading flowchart that I spent hours drawing. It's so valuable that you can comb through Glide's main flow carefully.

V. Summary

At this point, Glide's entire load process analysis is over, and you can see that the core logic of Glide is clustered in the in () method, which has a sophisticated and complex design. Source analysis in this part is time consuming, but if you really go into it step by step, you may feel insightful on the way to the advanced Android.At present, the Android mainstream triple library source analysis series has carried out detailed source code analysis on the network libraries (OkHttp, Retrofit) and image loading libraries (Glide). Next, we will conduct in-depth analysis on the core source code of the database framework GreenDao. Please look forward to ~

Reference link:

1. Glide V4.8.0 Source

2,Understanding Glide's execution process from a source Perspective

3,glide source analysis

appreciate

If this library is of great help to you, you are willing to support the further development of this project and the ongoing maintenance of this project.You can scan the QR code below and let me have a cup of coffee or beer.Thank you very much for your donation.Thank you!

Contanct Me

# WeChat:

Welcome to my WeChat: bcce5360

# WeChat Group:

If you can't sweep code to join the WeChat group, please invite your friends who want to join the WeChat group, and add me to WeChat to pull you into the group.

The QQ group:

2,000 people QQ group, Awesome-Android learning and communication group, QQ group number: 959936182, welcome to join ~

About me

Thank you for reading this article. I hope you can share it with your friends or technical groups. It means a lot to me.

Hope we can be friends at Github,Nuggets Share your knowledge last time.

Twelve original articles were published, 0 won, 3400 visits
Private letter follow

Posted by GetReady on Thu, 16 Jan 2020 18:29:25 -0800