preface
If you want to grasp the essence of Glide image loading, the first thing to clear Glide image loading process
Options = new RequestOptions().placeholder(R.drawable.loading); // It needs a Glide. With (this).load(url).apply(options).into(imageView);Copy the code
Well, you can see the use of Glide is very simple, but often the more simple behind, the more hidden the complex implementation, next we step by step analysis Glide 4.9 a load process
A.
public class Glide implements ComponentCallbacks2 { public static RequestManager with(@NonNull Context context) { // 1. Call getRetriever to get a RequestManagerRetriever // 2. Call the RequestManagerRetriever. Get access to a manager RequestManager describes a picture to load requestreturngetRetriever(context).get(context); } private static RequestManagerRetriever getRetriever(@nullable Retriever Context Context) {// This Retriever is used to retrieve a RequestManager object from this Retriever. See SystemRetriever in the Android Framework source codereturnGlide.get(context).getRequestManagerRetriever(); }}Copy the code
Ok, you can see Glide. With operation, two main things are done
- Get a RequestManagerRetriever object from Glide. GetRetriever, which is described as the request-managed retriever
- Call Glide. Get to get the Glide object
- Call Glide getRequestManagerRetriever, obtain RequestManagerRetriever object
- Call getRequestManagerRetriever. Get access to a RequestManager
Let’s go through this step by step, first getting the RequestManagerRetriever
A) Obtain Glide object
Can only from the above analysis, RequestManagerRetriever is through the Glide getRequestManagerRetriever get, so you need to first obtain the Glide object instance, so let’s look at how the Glide structure
public class Glide implements ComponentCallbacks2 {
private static volatile Glide glide;
public static Glide get(@NonNull Context context) {
if (glide == null) {
synchronized (Glide.class) {
if(glide == null) { checkAndInitializeGlide(context); }}}return glide;
}
private static volatile boolean isInitializing;
private static void checkAndInitializeGlide(@NonNull Context context) {
if(isInitializing) {// throw a quadratic initial exception} isInitializing =true; InitializeGlide (context); isInitializing =false; } private static void initializeGlide(@nonnull Context Context) {// Create an instance of GlideBuilder(). Glide initializeGlide(Context, new GlideBuilder()); } private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder) { Context applicationContext = context.getApplicationContext(); / / 1. Get the @ GlideModule GeneratedAppGlideModuleImpl generated annotation driven and GeneratedAppGlideModuleFactory GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules(); . / / 2. Try to obtain RequestManager annotationGeneratedModule generated from the annotation of factory object RequestManagerRetriever. RequestManagerFactory factory = annotationGeneratedModule ! = null ? annotationGeneratedModule.getRequestManagerFactory() : null; / / 3. This request is added to Glide Builder manager the construction of the factory Builder. SetRequestManagerFactory (factory); . Glide = Build.build (applicationContext); // 4. . / / 5. Registered a component with the Application of callback, used to detect system Config change and low memory footprint signal applicationContext. RegisterComponentCallbacks (glide); // Store it in a static member variable Glide. Glide = Glide; }}Copy the code
As you can see from the above code, Glide objects are interprocess singletons that are first created by calling the initializeGlide method, which handles transactions as follows
- First look for the @glidemodule annotation generation class (the implementation is omitted here)
- Then add a RequestManagerFactory inside the GlideBuilder
- Then we built a Glide object
Let’s focus on Glide object creation
Public final class GlideBuilder {// Manage thread pool private Engine Engine; // 1. Thread pool private GlideExecutorsourceExecutor; private GlideExecutor diskCacheExecutor; private GlideExecutor animationExecutor; // 2. Memory cache policy Private MemorySizeCalculator MemorySizeCalculator; private MemoryCache memoryCache; // 3. Private BitmapPool BitmapPool; private ArrayPool arrayPool; // 4. DiskCache and request build Factory private diskcache. Factory diskCacheFactory; private RequestManagerFactory requestManagerFactory; @NonNull Glide build(@NonNull Context context) { /* 1. Thread pools */ // 1.1 Network operations use thread poolsif (sourceExecutor == null) {
sourceExecutor = GlideExecutor.newSourceExecutor(); } // the thread pool used by the disk cacheif(diskCacheExecutor == null) { diskCacheExecutor = GlideExecutor.newDiskCacheExecutor(); } // 1.3 Thread pool to execute the animationif(animationExecutor == null) { animationExecutor = GlideExecutor.newAnimationExecutor(); } /* 2. Memory cache */ // / 2.1 Describe a memory calculator, intelligent load image size, determine its need for memory spaceif(memorySizeCalculator == null) { memorySizeCalculator = new MemorySizeCalculator.Builder(context).build(); } // 2.2 LRU resource memory cacheif(memoryCache == null) { memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize()); } // 2.3 Bitmap multiplexing pool of LRUif (bitmapPool == null) {
int size = memorySizeCalculator.getBitmapPoolSize();
if (size > 0) {
bitmapPool = new LruBitmapPool(size);
} else{ bitmapPool = new BitmapPoolAdapter(); } // 2.4 LRU array multiplexing poolif(arrayPool == null) { arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes()); } /* 3 disk cache factory */if(diskCacheFactory == null) { diskCacheFactory = new InternalCacheDiskCacheFactory(context); } // 4. RequestManagerRetriever RequestManagerRetriever RequestManagerRetriever = new RequestManagerRetriever(requestManagerFactory); // 5. Build an execution engine that manages thread pools and cachesif (engine == null) {
engine =
new Engine(
memoryCache,
diskCacheFactory,
diskCacheExecutor,
sourceExecutor, GlideExecutor.newUnlimitedSourceExecutor(), GlideExecutor.newAnimationExecutor(), isActiveResourceRetentionAllowed); }... // 6. Create Glide objectreturn new Glide(
context,
engine,
memoryCache,
bitmapPool,
arrayPool,
requestManagerRetriever,
connectivityMonitorFactory,
logLevel,
defaultRequestOptions.lock(),
defaultTransitionOptions,
defaultRequestListeners,
......);
}
}
Copy the code
Well, as you can see, the construction process of Glide object is extremely complex. I have adjusted some of the data, and their flow is as follows
- buildThe thread pool
- Different thread pools are built according to different task characteristics
- buildMemory cache Policy
- Memory calculator
- LRU resource memory cache
- Array object reuse pool
- Bitmap reuse pool
- A RequestManagerRetriever is created that describes the accessor of the requested managed object
- Build engine classes for organizing and scheduling thread pools and caches
- Create Glide Object
Glide is created as follows
The creation of Glide
public class Glide implements ComponentCallbacks2 { private final Registry registry; Glide(......) {// 1 This. Engine = engine; // 2 This. this.bitmapPool = bitmapPool; this.arrayPool = arrayPool; this.memoryCache = memoryCache; this.requestManagerRetriever = requestManagerRetriever; this.connectivityMonitorFactory = connectivityMonitorFactory; // 2. Use Registry to register Encoder and Decoder for Glide defaultRequestOptions.getOptions().get(Downsampler.DECODE_FORMAT); bitmapPreFiller = new BitmapPreFiller(memoryCache, bitmapPool, decodeFormat); final Resources resources = context.getResources(); Registry = new Registry(); Registry.register (new DefaultImageHeaderParser()); . GlideContext = New glideContext (Context, arrayPool, Registry, imageViewTargetFactory, defaultRequestOptions, defaultTransitionOptions, defaultRequestListeners, engine, isLoggingRequestOriginsEnabled,logLevel);
}
Copy the code
The Glide object is created as follows
- Import the data from GlideBuilder
- Build a Registry that registers a number of codecs
- Context objects are built to hold data that may be used
2. Obtain the RequestManagerRetriever
In creating glideBuilder.build, we see that it new a RequestManagerRetriever object and passes it into the Glide object, So by Glide. GetRequestManagerRetriever can easily access to RequestManagerRetriever this object
Let’s look at how to use RequestManagerRetriever. Get () to obtain RequestManager object
3) Obtain the RequestManager
public class RequestManagerRetriever implements Handler.Callback {
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)) { // 1. If it's on the main thread and the Context is not Applicationif (context instanceof FragmentActivity) {
return get((FragmentActivity) context);
} else if (context instanceof Activity) {
return get((Activity) context);
} else if(the context instanceof ContextWrapper) {/ / continuously find its BaseContext, judge whether can target matching with FragmentActivity/Activity, etcreturnget(((ContextWrapper) context).getBaseContext()); } // 2. If MainThread or context is not the type of Application, use ApplicationManagerreturngetApplicationManager(context); }}Copy the code
Can see RequestManagerRetriever. The get method can judge the type of the Context
- If the Context is in the main thread and not of type Application, find the Activity that it depends on
- If it is not a main thread or a Context of type Application, use ApplicationManager
Why do you want to find the Activity first, and why? Let’s take a look at the overloading method of get with the Activity parameter
1. Obtain the RequestManager for the Activity
public class RequestManagerRetriever implements Handler.Callback {
public RequestManager get(@NonNull Activity activity) {
if(Util. IsOnBackgroundThread ()) {/ / were it not for the main thread, direct access to the Application types of RequestManagerreturn get(activity.getApplicationContext());
} else{// Do not load assertNotDestroyed(activity) while the activity is destroyed; / / get its FragmentManager android. App. FragmentManager FM = activity. GetFragmentManager ();returnfragmentGet( activity, fm, /*parentHint=*/ null, isActivityVisible(activity)); } } private RequestManager fragmentGet(@NonNull Context context, @NonNull android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint, boolean isParentVisible) { // 1. Get a RequestManagerFragment from the Activity, For monitoring the Activity statement cycle RequestManagerFragment current = getRequestManagerFragment (FM, parentHint isParentVisible); / / 2. Obtain fragments stored in the current page request manager RequestManager RequestManager = current. GetRequestManager (); // 3. Create a request manager and save it in RequestManagerFragmentif(requestManager == null) { Glide glide = Glide.get(context); requestManager = factory.build(glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context); current.setRequestManager(requestManager); } // Return the request managerreturnrequestManager; } / / describe a RequestManagerFragment cache to be FragmentManager add final Map < android. App. FragmentManager, RequestManagerFragment> pendingRequestManagerFragments = new HashMap<>(); private RequestManagerFragment getRequestManagerFragment( @NonNull final android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint, Boolean isParentVisible) {// 2.1 Try to get the Fragment RequestManagerFragment Current = from the FragmentManager (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG); // 2.2 If it does not exist, add oneif(current = = null) {/ / 2.3 from pendingRequestManagerFragments cache to get a current = pendingRequestManagerFragments. Get (FM);if(current == null) {// 2.3.1 create and update the cache current = new RequestManagerFragment(); . // 2.3.2 Adding to the cache waiting to be added // Because there is a delay in adding to the FragmentManager, In this way to prevent the same time created two RequestManagerFragment object is added to the Activity of pendingRequestManagerFragments. Put (FM, current); // 2.3.3 Adding to the FragmentManager fm.beginTransaction().add(current, FRAGMENT_TAG).commitallowingStateloss (); ObtainMessage (ID_REMOVE_FRAGMENT_MANAGER, ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget(); }}returncurrent; }}Copy the code
Ok, so you can see that the Get method for the RequestManagerRetriever basically adds a RequestManagerFragment instance to the Activity page so that it can be used to listen to the Activity’s lifecycle, The Fragment is then injected with a RequestManager, and the details of its processing are annotated in the code
- One very interesting detail is that there is a delay in adding fragments to the FragmentManger, in order to prevent two RequestManagerFragments being created and added to the FragmentManager at the same time, So it USES the pendingRequestManagerFragments caching
2. Obtain the Application RequestManager
public class RequestManagerRetriever implements Handler.Callback {
private volatile RequestManager applicationManager;
private final RequestManagerFactory factory;
private RequestManager getApplicationManager(@NonNull Context context) {
// Either an application context or we're on a background thread. if (applicationManager == null) { synchronized (this) { if (applicationManager == null) { Glide glide = Glide.get(context.getApplicationContext()); applicationManager = factory.build(glide, new ApplicationLifecycle(), new EmptyRequestManagerTreeNode(), context.getApplicationContext()); } } } return applicationManager; }}Copy the code
It’s easy to build a singleton RequestManager that handles all context requests
3) review
Glide. With builds the RequestManager for the Context
- Gets the Glide object for an interprocess singleton
- The thread pool
- Buffer pool
- codecs
- Gets the RequestManagerRetriever object for the inter-process singleton
- Use the RequestManagerRetriever to create the RequestManager of the current context
- If the Activity can be bound, add a RequestManagerFragment for the Activity with an internal ReqeustManager object to subsequently manage the processing of Glide requests directly based on the Activity’s lifecycle
- If an Activity is not bound, the applicationManager that gets a singleton is dedicated to handling such requests
Next, let’s look at the requestManager.Load method
2. The load
We analyze the requestManager.Load method using the most familiar load network image
public class RequestManager implements LifecycleListener, ModelTypes<RequestBuilder<Drawable>> { public RequestBuilder<Drawable> load(@Nullable String string) { // 1. Call asDrawable to create a Drawable image load request // 2. Call load to load the resource inreturn asDrawable().load(string);
}
public RequestBuilder<Drawable> asDrawable() {
return as(Drawable.class);
}
public <ResourceType> RequestBuilder<ResourceType> as(
@NonNull Class<ResourceType> resourceClass) {
returnnew RequestBuilder<>(glide, this, resourceClass, context); }}Copy the code
The requestManager. load method first calls asDrawable and builds a RequestBuilder that describes a request to load an image with a Drawable target resource
And then we call the RequestBuilder. Load method and we pass in the data source that’s being loaded. So what does this load method do
public class RequestBuilder<TranscodeType> extends BaseRequestOptions<RequestBuilder<TranscodeType>>
implements Cloneable, ModelTypes<RequestBuilder<TranscodeType>> {
public RequestBuilder<TranscodeType> load(@Nullable String string) {
returnloadGeneric(string); } @nullable Private Object Model; Private Boolean isModelSet; private Boolean isModelSet; private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) { this.model = model; isModelSet =true;
returnthis; }}Copy the code
Ok, so you can see that at this point, the RequestBuilder is built, and now it’s ready to execute the request, so let’s look at the Into method of the RequestBuilder
3. RequestBuilder. Into
public class RequestBuilder<TranscodeType> extends BaseRequestOptions<RequestBuilder<TranscodeType>> implements Cloneable, ModelTypes<RequestBuilder<TranscodeType>> { public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) { ...... // 1. RequestOptions BaseRequestOptions<? > requestOptions = this; RequestBuilder inherits BaseRequestOptions directlyif(! requestOptions.isTransformationSet() && requestOptions.isTransformationAllowed() && view.getScaleType() ! = null) { switch (view.getScaleType()) {caseCENTER_CROP: RequestOptions = requestOptions.clone().optionalCenterCrop();break; . }} // 3. Call the into method to create and execute the requestreturnInto (/ / 2. Will the View and encapsulate ViewTarget glideContext. BuildImageViewTarget (View, transcodeClass), / * targetListener = * / null, requestOptions, Executors.mainThreadExecutor()); }}Copy the code
You can see that the main steps in the into method are as follows
- Options are configured according to the ScaleType of the ImageView
- Encapsulate the View as a ViewTarget
- The overloaded method into is called to perform subsequent build request operations
Let’s take CenterCrop as an example and see how it configures the zoom effect
1) Configure Options
public abstract class BaseRequestOptions<T extends BaseRequestOptions<T>> implements Cloneable {
public T optionalCenterCrop() {// 1. OptionalTransform is called // DownsampleStrategy describes the downsampling compression strategy // CenterCrop describes the image change modereturnoptionalTransform(DownsampleStrategy.CENTER_OUTSIDE, new CenterCrop()); } final T optionalTransform(@NonNull DownsampleStrategy downsampleStrategy, @NonNull Transformation<Bitmap> transformation) { ...... // 2. Add the downsampleStrategy to options downsample(downsampleStrategy); // 3. Add the changes in the images to the translationsreturn transform(transformation, /*isRequired=*/ false); } public T downsample(@nonnull DownsampleStrategy strategy) {// 2.1 calledset, save the downsampling policy to optionsreturn set(DownsampleStrategy.OPTION, Preconditions.checkNotNull(strategy));
}
private Options options = new Options();
public <Y> T set(@NonNull Option<Y> option, @NonNull Y value) { ... // 2.2 Add to the options cache options.set(option, value);returnselfOrThrowIfLocked(); } T transform(@nonnull Transformation<Bitmap> Transformation, Boolean isRequired) {// DrawableTransformation DrawableTransformation = New DrawableTransformation(Transformation, isRequired); transform(Bitmap.class, transformation, isRequired); Transform (Drawable. Class, drawableTransformation, isRequired); // The Drawable type is......returnselfOrThrowIfLocked(); } private Map<Class<? >, Transformation<? >> transformations = new CachedHashCodeArrayMap<>(); <Y> T transform(@NonNull Class<Y> resourceClass, @NonNull Transformation<Y> transformation, String isRequired) {// 3.2 has been added to the doubling cache. Put (resourceClass, transformation);returnselfOrThrowIfLocked(); }}Copy the code
You can see that in addition to the image changes, you can also set the sampling mode, which is saved in translations and options, respectively
The GlideContext is a decorator of the Context. It adds Glide data to the Context of the Application type. How does it build a ViewTarget
2) Build ViewTarget
public class GlideContext extends ContextWrapper { private final ImageViewTargetFactory imageViewTargetFactory; public <X> ViewTarget<ImageView, X> buildImageViewTarget( @NonNull ImageView imageView, @nonnull Class<X> transcodeClass) {// Call the factory Class to create a ViewTarget for the imageViewreturn imageViewTargetFactory.buildTarget(imageView, transcodeClass);
}
}
public class ImageViewTargetFactory {
@NonNull
@SuppressWarnings("unchecked") public <Z> ViewTarget<ImageView, Z> buildTarget(@NonNull ImageView view, @nonnull Class<Z> clazz) {// create different ViewTarget objects according to the type of target encoding, because we do not have asBitmap, so here is Drawableif (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{... }}}Copy the code
The GlideContext factory class creates the ViewTarget of the ImageView, which describes the View target that will be used after the image processing is complete
3) Into overload mode
Now that the ViewTarge is built, it’s time to examine the overloaded INTO method to see how it builds the request
public class RequestBuilder<TranscodeType> extends BaseRequestOptions<RequestBuilder<TranscodeType>> implements Cloneable, ModelTypes<RequestBuilder<TranscodeType>> { private <Y extends Target<TranscodeType>> Y into(@NonNull Y target, @Nullable RequestListener<TranscodeType> targetListener, BaseRequestOptions<? > options, Executor callbackExecutor) { ...... Request Request = buildRequest(target, targetListener, Options, callbackExecutor); . // Handle the collision between the previous request and the new request // 2. Bind this Glide request to the ViewTarget target.setrequest (request); Requestmanager.track (target, request); // 3.returntarget; }}Copy the code
As you can see, the overloaded method of into mainly performs the following steps
- Pass the parameters into the Build Request Request
- Bind the request for the ViewTarget
- Call requestManger.track to distribute the request
4) review
RequestBuilder. Into mainly does the following
- The strategies for building sample compression and image changes based on the ImageView are saved in Options and Transform
- Build a ViewTarget that describes the View object that the request will use
- Call into overload method
- Building the Request object
- Bind the Request to the ViewTarget
- Submit the request to the RequestManger to execute the request
At this point our Glide.with().load().into is done and will eventually build a Request for the RequestManger to distribute and execute
Now let’s see how the RequestManager executes requests
RequestManager Performs requests
public class RequestManager implements LifecycleListener, ModelTypes<RequestBuilder<Drawable>> { private final RequestTracker requestTracker; synchronized void track(@NonNull Target<? > target, @NonNull Request request) { ...... / / 1. Perform requests requestTracker. RunRequest (request); } } public class RequestTracker { private final Set<Request> requests = Collections.newSetFromMap(new WeakHashMap<Request, Boolean>()); private final List<Request> pendingRequests = new ArrayList<>(); public void runRequest(@NonNull Request request) { requests.add(request);if(! IsPaused) {// 2. Call request.begin to execute the task request.begin(); }else{... } } } public final class SingleRequest<R> implements Request, SizeReadyCallback, ResourceCallback, FactoryPools.Poolable { public synchronized voidbegin() {...if(Util.isValidDimensions(overrideWidth, overrideHeight)) { // 3. OnSizeReady (overrideWidth, overrideHeight); }else{... }... } private Engine engine; private int width; private int height; public synchronized void onSizeReady(int width, int height) { ...... // 4. LoadStatus = Engine. Load (......) ; . }}Copy the code
Ok, you can see that onSizeReady is finally called to build the executable task, so let’s analyze this process
1) Task construction
public class Engine implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {
private final Jobs jobs; public synchronized <R> LoadStatus load(...) = keyfactory.buildKey (Model, signature, width, height, doubling, doubling, doubling, doubling, doubling, doubling, doubling) resourceClass, transcodeClass, options); // 2.1 Try to find the cache of this key from ActiveResources. EngineResource<? > active = loadFromActiveResources(key, isMemoryCacheable);if (active != null) {
// 若缓存存在, 则直接回调 onResourceReady 处理后续操作
cb.onResourceReady(active, DataSource.MEMORY_CACHE);
.......
returnnull; } // 2.2 Try to find the resource from LruResourceCache. EngineResource<? > cached = loadFromCache(key, isMemoryCacheable);if(cached ! = null) {// call back onResourceReady to handle subsequent operations cb.onResourceReady(cached, datasource.memory_cache);returnnull; } // 3. 'EngineJob' <? > current = jobs.get(key, onlyRetrieveFromCache);if(current ! = null) {current.addCallback(cb, callbackExecutor); . // Return to the loading statereturnnew LoadStatus(cb, current); } EngineJob<R> EngineJob = EngineJob.build (key, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache); DecodeJob<R> DecodeJob = decodeJobfactory.build (...... , engineJob); Jobs. put(key, engineJob); . // 3.2.4 Executing task enginejob. start(decodeJob); . }}Copy the code
Okay, so you can see that things in engine.Load are very important
- Build the key for this request
- Look for the resource corresponding to the key in the memory cacheIf there is one, return onResourceReady to indicate that the resource is ready
- From the ActiveResources cache
- Look in the LruResourceCache cache
- Find the task corresponding to the key from the cache
- If yes, you do not need to obtain resources again
- Build tasks
- Build engine task EngineJob
- The engine’s task is DecodeJob
- Add tasks to the cache to prevent multiple builds
- Perform EngineJob
Ok, you can see that the memory cache processing is done in the Engine. If neither memory cache hits, the task is built and executed. Let’s look at how the task executes
2) The execution of EngineJob
class EngineJob<R> implements DecodeJob.Callback<R>, Poolable { private final GlideExecutor diskCacheExecutor; private DecodeJob<R> decodeJob; public synchronized void start(DecodeJob<R> decodeJob) { this.decodeJob = decodeJob; . / / the thread pool GlideExecutor executor = decodeJob willDecodeFromCache ()? diskCacheExecutor : getActiveSourceExecutor(); Execute (decodeJob); // Execute the task executor.execute(decodeJob); }}Copy the code
It is simply left to the thread pool to execute the DecodeJob
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback, Runnable, Comparable<DecodeJob<? >>, Poolable { @Override public voidrun() { try { ...... // Call runWrapped runWrapped(); } catch (CallbackException e) { ...... } } private voidrunWrapped() {
switch (runReason) {
caseStage = getNextStage(stage.initialize); CurrentGenerator = getNextGenerator(); // 2. // 3. Execute a task runGenerators();break; . }} private Stage getNextStage(Stage current) {switch (current) {// 1.1 Determine whether disk resource cache can be read. If not, continue to look for the appropriate scenariocase INITIALIZE:
returndiskCacheStrategy.decodeCachedResource() ? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE); // 1.2 Determine whether the source cache can be read from disk. If yes, return stage. DATA_CACHE. If no, continue searchingcase RESOURCE_CACHE:
returndiskCacheStrategy.decodeCachedData() ? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE); // 1.3 If only data can be obtained from the cache, FINISH directly, otherwise return stage. SOURCE, which means to load a new resourcecase DATA_CACHE:
return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
case SOURCE:
case FINISHED:
return Stage.FINISHED;
default:
throw new IllegalArgumentException("Unrecognized stage: " + current);
}
}
private DataFetcherGenerator getNextGenerator() {
switch (stage) {
caseRESOURCE_CACHE: // The implementer of 2.1 resource disk cachereturn new ResourceCacheGenerator(decodeHelper, this);
caseDATA_CACHE: // 2.2 Perform the source data disk cachereturn new DataCacheGenerator(decodeHelper, this);
caseSOURCE: // 2.3 No cache, get the SOURCE of the data executantreturn new SourceGenerator(decodeHelper, this);
case FINISHED:
return null;
default:
throw new IllegalStateException("Unrecognized stage: " + stage);
}
}
private void runGenerators() {... boolean isStarted =false;
while(! isCancelled && currentGenerator ! = null / 3.1 / call currentGenerator. StartNext () carry out the tasks of the current scene &&! (isStarted = currentGenerator startNext ())) {/ / 3.2 if failure, then for the next scene continues executing stage = getNextStage (stage); currentGenerator = getNextGenerator();if (stage == Stage.SOURCE) {
reschedule();
return; }}... }}Copy the code
The runWrapper method of DecodeJob does the following transactions
- Get the Decode scenario through getNextStage
- Builds the processor DataFetcherGenerator for the current scenario
- Call runGenerators to process the task
- Data is preferentially fetched from the disk cache of the Resource
- The second priority is to retrieve Data from the disk cache of Data
- The final solution reobtains the source data
scenario | Scene description | Scenario actuator |
---|---|---|
Stage.RESOURCE_CACHE | Retrieves data from cached resources on disk | ResourceCacheGenerator |
Stage.DATA_CACHE | Gets the data from the source data cached on disk | DataCacheGenerator |
Stage.SOURCE | Rerequest data | SourceGenerator |
The Resource and Data disk cache of interest can be analyzed on their own, we will look at the SourceGenerator to obtain the source Data scheme
1. SourceGenerator Obtains the data stream
class SourceGenerator implements DataFetcherGenerator, DataFetcher.DataCallback<Object>, DataFetcherGenerator.FetcherReadyCallback { private final DecodeHelper<? > helper; public booleanstartNext() {... loadData = null; boolean started =false;
while(! started && hasNextModelLoader()) { // 1. From the data loading set of DecodeHelper, get a data loader loadData = helper.getloaddata ().get(loadDataListIndex++);if(loadData ! = null && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource()) || helper.hasLoadPath(loadData.fetcher.getDataClass()))) { started =true; / / 2. Use the loader fetcher perform data loading loadData. The fetcher. LoadData (helper, getPriority (), this); }}returnstarted; }}Copy the code
Okay, SourceGenerator has two main steps
- Call decodeHelper.getLoadData to get the data loader for the current request
- Call fetcher.loadData in the loader to actually perform the data loading
1) Obtain the data loader LoadData
final class DecodeHelper<Transcode> { private final List<LoadData<? >> loadData = new ArrayList<>(); private GlideContext glideContext; private Object model; private boolean isLoadDataSet; List<LoadData<? >>getLoadData() {
if(! isLoadDataSet) { isLoadDataSet =true; loadData.clear(); List<ModelLoader<Object,? >> modelLoaders = glideContext.getRegistry().getModelLoaders(model); // Go through each modelLoadersfor(int i = 0, size = modelLoaders.size(); i < size; I++) {// 2. LoadData ModelLoader<Object,? > modelLoader = modelLoaders.get(i); LoadData<? > current = modelLoader.buildLoadData(model, width, height, options);if(current ! = null) {// add to cache loadData.add(current); }}}returnloadData; }}Copy the code
It’s going to find a ModelLoader implementation class, and it’s going to use the handles method on that implementation class to see if it can load the model. Its implementation class is HttpGlideUrlLoader. How does it build a LoadData object
public class HttpGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> { @Nullable private final ModelCache<GlideUrl, GlideUrl> modelCache; @Override public LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height, @NonNull Options options) { GlideUrl url = model; . int timeout = options.get(TIMEOUT); // Create a LoadData object and instantiate an HttpUrlFetcher to itreturnnew LoadData<>(url, new HttpUrlFetcher(url, timeout)); }}Copy the code
Ok, so you can see that for the URL to load, the fetcher is an instance of HttpUrlFetcher, so let’s look at the data loading process
2) Perform data loading
After obtaining the data loader, the startNext of the SourceGenerator calls the loadData of its fetcher to perform the data loading. We will analyze this process
public class HttpUrlFetcher implements DataFetcher<InputStream> { public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) { long startTime = LogTime.getLogTime(); InputStream result = loadDataWithRedirects(glideur.tourl (), 0); null, glideUrl.getHeaders()); Callback.ondataready (result); // callback inputStream to callback.ondataready (result); } catch (IOException e) { ...... callback.onLoadFailed(e); } finally { ...... }}}Copy the code
HttpUrlFetcher uses HttpConnection to make a network request and fetch the data stream. At this point, the data resource is fetched. The most important thing to do is to process the data. It throws the InputStream as a callback
The onDataFetcherReady that will eventually trace back to the DecodeJob processes the InputStream
2. DecodeJob Decodedata stream
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback, Runnable, Comparable<DecodeJob<? >>, Poolable { private Key currentSourceKey; private Object currentData; private DataSource currentDataSource; private DataFetcher<? > currentFetcher; @Override public void onDataFetcherReady(KeysourceKey, Object data, DataFetcher<? > fetcher, DataSource dataSource, Key attemptedKey) { this.currentSourceKey =sourceKey; CurrentData = data; This.currentfetcher = fetcher; This. currentDataSource = dataSource; // Data source: url is an enumeration of type REMOTE, indicating that...... is obtained from REMOTEif(Thread.currentThread() ! = currentThread) { ...... }else{try {// Call decodeFromRetrievedData decodeFromRetrievedData(); } finally { ...... } } } private voiddecodeFromRetrievedData() { Resource<R> resource = null; Resource = decodeFromData(/*HttpUrlFetcher*/currentFetcher, /*HttpUrlFetcher*/currentFetcher, /*InputStream*/currentData,/*REMOTE*/ currentDataSource); } catch (GlideException e) { ...... }if(resource ! = null) {// 2. NotifyEncodeAndRelease (Resource, currentDataSource) }else{... }}}Copy the code
DecodeJob decodes a data stream as follows
- The first step is to decode the InputStream as Resource (not Android Resource)
- Cache and display of Resource
Let’s first look at how does it parse the data into a Resource
1) decoding InputStream
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback, Runnable, Comparable<DecodeJob<? >>, Poolable { private <Data> Resource<R> decodeFromData(DataFetcher<? > fetcher, Data data, DataSource dataSource) throws GlideException { try { ...... Resource<R> result = decodeFromFetcher(data, dataSource); .returnresult; } finally { } } private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource) throws GlideException { // 1. LoadPath LoadPath<Data,? , R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass()); // 2. Parse the data through the parserreturnrunLoadPath(data, dataSource, path); } private <Data, ResourceType> Resource<R> runLoadPath(Data data, DataSource dataSource, LoadPath<Data, ResourceType, R> path) throws GlideException { Options options = getOptionsWithHardwareConfig(dataSource); // 2.1 Obtain a data regenerator based on the data type, the data obtained is InputStream, DataRewinder<Data> rewinder = glidecontext.getregistry ().getrewinder (Data); Try {// 2.2 Transfer the task of resolving the resource to the loadPath.load methodreturnpath.load( rewinder, options, width, height, new DecodeCallback<ResourceType>(dataSource)); } finally { rewinder.cleanup(); }}}Copy the code
You can see that to parse the data, a LoadPath is first built, then a DataRewinder of type InputStreamRewinder is created, and finally the data is parsed into the loadPath.load method
Let’s see what this loadPath.load does
public class LoadPath<Data, ResourceType, Transcode> {
public Resource<Transcode> load(DataRewinder<Data> rewinder, @NonNull Options options, int width,
int height, DecodePath.DecodeCallback<ResourceType> decodeCallback) throws GlideException {
......
try {
returnloadWithExceptionList(rewinder, options, width, height, decodeCallback, throwables); } finally { ...... } } private final List<? extends DecodePath<Data, ResourceType, Transcode>> decodePaths; 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; // Iterate through the internally stored DecodePath set and parse the data through themfor(int i = 0, size = decodePaths.size(); i < size; i++) { DecodePath<Data, ResourceType, Transcode> path = decodePaths.get(i); Result = path.decode(rewinder, width, height, options, decodeCallback); } catch (GlideException e) { ...... }... }returnresult; } } public class DecodePath<DataType, ResourceType, Transcode> { public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, int height, @NonNull Options options, DecodeCallback<ResourceType> callback) throws GlideException { // 1. Resource<ResourceType> decoded = decodeResource(width, height, options); / / 2. Call DecodeCallback. OnResourceDecoded processing resources among Resource < ResourceType > transformed = callback.onResourceDecoded(decoded); / / 3. Call ResourceTranscoder. Transcode between resources into the target resourcereturn transcoder.transcode(transformed, options);
}
Copy the code
You can see the DecodePath. Decode operation has three resources
- Call decodeResource to parse the source data into a resource
- The process of obtaining a Bitmap
- Call DecodeCallback. OnResourceDecoded processing resources
- Transform the Bitmap
- Call ResourceTranscoder. Transcode resources into the target resource
- Convert the Bitmap to the target type
Let’s first look at the operation that gets the Bitmap
Step1 gain Bitmap
private Resource<ResourceType> decodeResource(DataRewinder<DataType> rewinder, int width, int height, @nonnull Options Options) throws GlideException {try {decodeResourceWithList is calledreturn decodeResourceWithList(rewinder, width, height, options, exceptions);
} finally {
......
}
}
@NonNull
private Resource<ResourceType> decodeResourceWithList(DataRewinder<DataType> rewinder, int width,
int height, @NonNull Options options, List<Throwable> exceptions) throws GlideException {
Resource<ResourceType> result = null;
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)) { data = rewinder.rewindAndGet(); Result = decoder.decode(data, width, height, options); }}...if(result ! = null) {break; }}returnresult; }}Copy the code
Because the source data of this process is InputStream, its parser is StreamBitmapDecoder
public class StreamBitmapDecoder implements ResourceDecoder<InputStream, Bitmap> {
private final Downsampler downsampler;
public Resource<Bitmap> decode(@NonNull InputStream source, int width, int height, @NonNull Options options) throws IOException { ...... Try {// obtain a Resource<Bitmap> by sampling and compressing the data stream based on the request configurationreturndownsampler.decode(invalidatingStream, width, height, options, callbacks); } finally { ...... }}}Copy the code
You can see that it collects the Bitmap of the stream internally by sampling and compressing the stream using the downSampler.decode method
- The sampling strategy is what we passed in when we built the Request, and the details of the sampling compression are not the focus of this time
So let’s see, once we get the Resource, what do we do with that Resource
Step2 Transform Bitmap
As you can see, when we will source data parsed into corresponding resources, will call DecodeCallback. OnResourceDecoded processing resources, we see the process
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback, Runnable, Comparable<DecodeJob<? > >, Poolable { private final class DecodeCallback<Z> implements DecodePath.DecodeCallback<Z> { @Override public Resource<Z> OnResourceDecoded (@nonnull Resource<Z> decoded) {// Call the onResourceDecoded method of the external classreturnDecodeJob.this.onResourceDecoded(dataSource, decoded); } } private final DeferredEncodeManager<? > deferredEncodeManager = new DeferredEncodeManager<>(); <Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) { // 1. Invoke the Transformation. The Transformation to change operation Bitmap Class < Z > resourceSubClass = (Class Z < >) decoded. The get () getClass (); Transformation<Z> appliedTransformation = null; Resource<Z> transformed = decoded;if(dataSource ! = DataSource.RESOURCE_DISK_CACHE) { appliedTransformation = decodeHelper.getTransformation(resourceSubClass); transformed = appliedTransformation.transform(glideContext, decoded, width, height); }... // 2. Build disk cache strategy final EncodeStrategy EncodeStrategy; final ResourceEncoder<Z> encoder;if (decodeHelper.isResourceEncoderAvailable(transformed)) {
encoder = decodeHelper.getResultEncoder(transformed);
encodeStrategy = encoder.getEncodeStrategy(options);
} else{ encoder = null; encodeStrategy = EncodeStrategy.NONE; } Resource<Z> result = transformed; boolean isFromAlternateCacheKey = ! decodeHelper.isSourceKey(currentSourceKey);if (diskCacheStrategy.isResourceCacheable(isFromAlternateCacheKey, dataSource,
encodeStrategy)) {
......
final Key key;
switch (encodeStrategy) {
caseSOURCE: // SOURCE Bitmap = SOURCE cache key = new DataCacheKey(currentSourceKey, signature);break;
caseTRANSFORMED: TRANSFORMED: New ResourceCacheKey = new ResourceCacheKey(......) ;break;
default:
throw new IllegalArgumentException("Unknown strategy: "+ encodeStrategy); LockedResource<Z> lockedResult = LockedResource. Obtain (transformed); deferredEncodeManager.init(key, encoder, lockedResult); result = lockedResult; } // Return the bitmap after the transformreturnresult; }}Copy the code
As you can see in onResourceDecoded, we do the following mainly for intermediate resources
- Invoking the Transformation. The Transformation to change operation Bitmap
- Turns resources into target effects, such as CenterCrop set when building the request
- Build the key for the disk cache
- The original Bitmap is the Source cache
- After the Tranform is the Resource cache
Ok, after this method is executed, the resource is as good as we want it to be, and we just need to convert it to the target format to present it
Step3 convert the Bitmap to the target type
The target data is Drawable, so its converter is BitmapDrawableTranscoder
public class BitmapDrawableTranscoder implements ResourceTranscoder<Bitmap, BitmapDrawable> { private final Resources resources; @Nullable @Override public Resource<BitmapDrawable> transcode(@NonNull Resource<Bitmap> toTranscode, @ NonNull Options Options) {/ / call the LazyBitmapDrawableResource. Obtain access to the Resource < BitmapDrawable > instance of the objectreturnLazyBitmapDrawableResource.obtain(resources, toTranscode); } } public final class LazyBitmapDrawableResource implements Resource<BitmapDrawable>, Initializable { public static Resource<BitmapDrawable> obtain( @NonNull Resources resources, @Nullable Resource<Bitmap> bitmapResource) { ...... / / create a LazyBitmapDrawableResourcereturn new LazyBitmapDrawableResource(resources, bitmapResource);
}
private LazyBitmapDrawableResource(@NonNull Resources resources,
@NonNull Resource<Bitmap> bitmapResource) {
this.resources = Preconditions.checkNotNull(resources);
this.bitmapResource = Preconditions.checkNotNull(bitmapResource);
}
public BitmapDrawable get{// The Get method returns a BitmapDrawable objectreturnnew BitmapDrawable(resources, bitmapResource.get()); }}Copy the code
Ok, into the target data is also very simple, it will be our resolve to store bitmap to LazyBitmapDrawableResource inside, and then the outside world through the get method can get a BitmapDrawable object
Now, how does notifyEncodeAndRelease present the data
2) Cache and display of resources
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback, Runnable, Comparable<DecodeJob<? >>, Poolable { private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) { ...... NotifyComplete (result, dataSource); // 1. . Try {// 2. Cache data to diskif(deferredEncodeManager.hasResourceToEncode()) { deferredEncodeManager.encode(diskCacheProvider, options); } } finally { ... } } private Callback<R> callback; private void notifyComplete(Resource<R> resource, DataSource dataSource) { ...... // the DecodeJob is an EngineJob Callback. OnResourceReady (resource, dataSource); }}Copy the code
Good, can see DecodeJob decodeFromRetrievedData, mainly do the two operations
- The callback EngineJob onResourceReady resources ready
- Cache data to disk
Here we mainly look at EngineJob. OnResourceReady did what processing
class EngineJob<R> implements DecodeJob.Callback<R>,
Poolable {
@Override
public void onResourceReady(Resource<R> resource, DataSource dataSource) {
synchronized (this) {
this.resource = resource;
this.dataSource = dataSource;
}
notifyCallbacksOfResult();
}
void notifyCallbacksOfResult() {
ResourceCallbacksAndExecutors copy;
Key localKey; EngineResource<? >localResource;
synchronized (this) {
......
engineResource = engineResourceFactory.build(resource, isCacheable);
hasResource = true;
copy = cbs.copy();
incrementPendingCallbacks(copy.size() + 1);
localKey = key;
localResource = engineResource; } / / 1. Notice the upper Engine task completed the listener. OnEngineJobComplete (this,localKey, localResource); // 2. Call back to ImageViewTarget to display the resourcefor(final ResourceCallbackAndExecutor entry : copy) { entry.executor.execute(new CallResourceReady(entry.cb)); }}}Copy the code
EngineJob also has two steps
- One is to notify the upper level that the task is complete
- The other is a callback to the ImageViewTarget to display the resource
Step1 call back the Engine task
So what do we do on the top first
public class Engine implements EngineJobListener, MemoryCache.ResourceRemovedListener, EngineResource.ResourceListener { public synchronized void onEngineJobComplete( EngineJob<? > engineJob, Key key, EngineResource<? > resource) {if(resource ! = null) {// Add the loaded resource to the memory cacheif(resource.isCacheable()) { activeResources.activate(key, resource); }}... }}Copy the code
We know that Engine tries to read through the memory cache before the request is made, and it is not surprising to return to Engine to add the memory cache after the request is made
Step2 notify ImageViewTarget to display resources
Let’s take a look at the ImageViewTarget presentation process
public abstract class ImageViewTarget<Z> extends ViewTarget<ImageView, Z> implements Transition.ViewAdapter { public void onResourceReady(@NonNull Z resource, @Nullable Transition<? Super Z> transition) {// handle some transition changes, which were analyzed while building the Request, and I won't go into the implementation details hereif(transition == null || ! transition.transition(resource, this)) {setResourceInternal(resource);
} else{... } } private voidsetResourceInternal(@nullable Z resource) {// CalledsetResource
setResource(resource); . } } public class DrawableImageViewTarget extends ImageViewTarget<Drawable> { protected voidsetResource(@nullable Drawable Resource) {// Render view.setimAgedRawable (Resource); }}Copy the code
ImageViewTarget calls the setResource method overridden by the subclass to fill in the data, and a Glide image load is complete
4) Process review
At this point, the data source fetch of a request has been completed, and the process is as follows
- Building tasks
- Build the key for this request
- Look for the resource corresponding to the key in the memory cacheIf there is one, return onResourceReady to indicate that the resource is ready
- From the ActiveResources cache
- Look in the LruResourceCache cache
- Find the task corresponding to the key from the cache
- If yes, you do not need to obtain resources again
- Build engine task EngineJob
- The engine’s task is DecodeJob
- Add tasks to the cache to prevent multiple builds
- Perform EngineJob
- Task execution
- Get the Decode scenario through getNextStage
- Builds the processor DataFetcherGenerator for the current scenario
- Call runGenerators to process the task
- Data is preferentially fetched from the disk cache of the Resource
- The second priority is to retrieve Data from the disk cache of Data
- The final solution reobtains the Source data
- Source Obtains network data stream InputStream from HttpUrlFetcher
- DecodeJob’s onDataFetcherReady handles the InputStream
- Parsing InputStream
- Parse the InputStream into a Bitmap using Downsampler
- Invoking the Transformation. The Transformation to change operation Bitmap
- Turn the Bitmap into a Drawable
- Resource caching and display
- Engine Performs memory caching
- ViewTarget Displays resources
- DecodeJob performs disk caching
- The original Bitmap corresponds to the SOURCE type
- The Bitmap after Transform corresponds to the RESOURCE type
- Parsing InputStream
conclusion
Through a process analysis, we know that the whole Glide image load mainly has the following steps
- with
- Gets the Glide object for the global singleton
- Gets the RequestManagerRetriever of the global singleton
- Creates the RequestManager corresponding to the current Context
- load
- Build a RequestBuilder object based on the width and height of, sampling method, transform changes, and so on
- into
- The strategies for building sample compression and image changes based on the ImageView are saved in Options and Transform
- Build a ViewTarget that describes the View object that the request will use
- Call into overload method
- Building the Request object
- Bind the Request to the ViewTarget
- Submit the request to the RequestManger to execute the request
- Execution of a request
- Building tasks
- Build the key for this request
- Look for the resource corresponding to the key in the memory cacheIf there is one, return onResourceReady to indicate that the resource is ready
- From the ActiveResources cache
- Look in the LruResourceCache cache
- Find the task corresponding to the key from the cache
- If yes, you do not need to obtain resources again
- Build engine task EngineJob
- The engine’s task is DecodeJob
- Add tasks to the cache to prevent multiple builds
- Perform EngineJob
- Task execution
- Get the Decode scenario through getNextStage
- Builds the processor DataFetcherGenerator for the current scenario
- Call runGenerators to process the task
- Data is preferentially fetched from the disk cache of the Resource
- The second priority is to retrieve Data from the disk cache of Data
- The final solution reobtains the Source data
- Source Obtains network data stream InputStream from HttpUrlFetcher
- DecodeJob’s onDataFetcherReady handles the InputStream
- Parsing InputStream
- Parse the InputStream into a Bitmap using Downsampler
- Invoking the Transformation. The Transformation to change operation Bitmap
- Change the Bitmap to BitmapDrawable
- Resource caching and display
- Engine Performs memory caching
- ViewTarget Displays resources
- DecodeJob performs disk caching
- The original Bitmap corresponds to the SOURCE type
- The Bitmap after Transform corresponds to the RESOURCE type
- Building tasks
After seeing the Glide loading process, I can see why it is the recommended image loading framework by Google. The internal details are very well handled and the GlideContext is used to describe the Glide context. With the clever integration of the Android Context, it really has a sense of reading the Android source code
However, this is only the simplest process, and Glide supports Gif, video loading operations, you can imagine how much logic its internal Decorder processing code, such a complex process, nested so many callbacks, no doubt increased the difficulty of reading the source code, if these operations are layered, And using interceptors to do that, I think, would make an image load a lot clearer