Glide is now the most widely used image loading framework, and has always wanted to work on it. Every time, it is deeply involved in it... This time, I will make a thorough analysis of it and try to cover the whole process and its details.
This article's Glide parsing is based on the latest version 4.11.0.
In fact, when loading images from the general network, you can simply analyze the general process. It is nothing more than to establish relevant request information, then request the request information through thread pool technology, and then convert the downloaded image file to display.
Let's start with a simple test using code, and then drill down
Glide.with(view.getContext()) .load(url) .into(view);
with()
Glide's with function provides us with different input parameters, and its final return object is RequestManager
Our test code uses Context, so let's trace this function here. In fact, the others are similar
@NonNull public static RequestManager with(@NonNull Context context) { return getRetriever(context).get(context); } @NonNull private static RequestManagerRetriever getRetriever(@Nullable Context context) { //Verification Context cannot be empty Preconditions.checkNotNull(context,"...."); return Glide.get(context).getRequestManagerRetriever(); } //Get the requestManagerRetriever property of Glide object directly public RequestManagerRetriever getRequestManagerRetriever() { return requestManagerRetriever; }
You can see the creation of the RequestManagerRetriever object. It must have been processed in Glide.get()
//Create Glide object by double lock singleton method public static Glide get(@NonNull Context context) { if (glide == null) { GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules(context.getApplicationContext()); synchronized (Glide.class) { if (glide == null) { checkAndInitializeGlide(context, annotationGeneratedModule); } } } return glide; } //Verify and initialize Glide object @GuardedBy("Glide.class") private static void checkAndInitializeGlide( @NonNull Context context, @Nullable GeneratedAppGlideModule generatedAppGlideModule) { // In the thread running initGlide(), one or more classes may call Glide.get(context). // Without this check, those calls could trigger infinite recursion. if (isInitializing) {//If it is being created, an error will be reported directly throw new IllegalStateException( "You cannot call Glide.get() in registerComponents()," + " use the provided Glide instance instead"); } isInitializing = true; //Real creation method initializeGlide(context, generatedAppGlideModule); isInitializing = false; } //Initialize Glide @GuardedBy("Glide.class") private static void initializeGlide( @NonNull Context context, @Nullable GeneratedAppGlideModule generatedAppGlideModule) { initializeGlide(context, new GlideBuilder(), generatedAppGlideModule); } @GuardedBy("Glide.class") @SuppressWarnings("deprecation") private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder, @Nullable GeneratedAppGlideModule annotationGeneratedModule) { ... //The RequestManagerFactory factory object is created to create the corresponding RequestManager RequestManagerRetriever.RequestManagerFactory factory = annotationGeneratedModule != null ? annotationGeneratedModule.getRequestManagerFactory() : null; builder.setRequestManagerFactory(factory); ... //Builder Design pattern, create Glide object Glide glide = builder.build(applicationContext); ... applicationContext.registerComponentCallbacks(glide); Glide.glide = glide; }
As you can see, the builder design pattern is used here to create the glide object. A property of RequestManagerFactory is set in builder. Let's see what we did in builder.
@NonNull Glide build(@NonNull Context context) { if (sourceExecutor == null) {//Create resource actuator sourceExecutor = GlideExecutor.newSourceExecutor(); } if (diskCacheExecutor == null) {//Disk cache actuator diskCacheExecutor = GlideExecutor.newDiskCacheExecutor(); } if (animationExecutor == null) {//Animation actuator animationExecutor = GlideExecutor.newAnimationExecutor(); } if (memorySizeCalculator == null) {//Memory size calculator, based on memorySizeCalculator = new MemorySizeCalculator.Builder(context).build(); } if (connectivityMonitorFactory == null) {//Factory connected for monitoring connectivityMonitorFactory = new DefaultConnectivityMonitorFactory(); } if (bitmapPool == null) {//bitmap pool int size = memorySizeCalculator.getBitmapPoolSize(); if (size > 0) { bitmapPool = new LruBitmapPool(size); } else { bitmapPool = new BitmapPoolAdapter(); } } if (arrayPool == null) { arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes()); } if (memoryCache == null) {//Memory cache policy, Lru cache policy is used by default memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize()); } if (diskCacheFactory == null) {//Hard disk cache policy diskCacheFactory = new InternalCacheDiskCacheFactory(context); } if (engine == null) {//Engine, which includes the created executor and cached information engine = new Engine( memoryCache, diskCacheFactory, diskCacheExecutor, sourceExecutor, GlideExecutor.newUnlimitedSourceExecutor(), animationExecutor, isActiveResourceRetentionAllowed); } if (defaultRequestListeners == null) {//Request listener, an immutable List defaultRequestListeners = Collections.emptyList(); } else { defaultRequestListeners = Collections.unmodifiableList(defaultRequestListeners); } //Here, a RequestManagerRetriever object is created. The parameter is the Factory object set previously RequestManagerRetriever requestManagerRetriever = new RequestManagerRetriever(requestManagerFactory); return new Glide(//Create Glide object context, engine, memoryCache, bitmapPool, arrayPool, requestManagerRetriever, connectivityMonitorFactory, logLevel, defaultRequestOptionsFactory, defaultTransitionOptions, defaultRequestListeners, isLoggingRequestOriginsEnabled, isImageDecoderEnabledForBitmaps); }
In GlideBuilder, many objects are created for us, including thread pool, buffer, cache size, Engine and RequestManagerRetriever.
Because when we call with() method, we use requestManagerRetriever. Let's take a look and see if there is any special processing in it
public RequestManagerRetriever(@Nullable RequestManagerFactory factory) { this.factory = factory != null ? factory : DEFAULT_FACTORY; handler = new Handler(Looper.getMainLooper(), this /* Callback */); } private static final RequestManagerFactory DEFAULT_FACTORY = new RequestManagerFactory() { @NonNull @Override public RequestManager build(@NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode requestManagerTreeNode, @NonNull Context context) { return new RequestManager(glide, lifecycle, requestManagerTreeNode, context); } };
A very simple constructor, which creates the handler object and sets the RequestManagerFactory object.
Back in the trunk, take a look at Glide's constructor.
Glide( @NonNull Context context, @NonNull Engine engine, @NonNull MemoryCache memoryCache, @NonNull BitmapPool bitmapPool, @NonNull ArrayPool arrayPool, @NonNull RequestManagerRetriever requestManagerRetriever, @NonNull ConnectivityMonitorFactory connectivityMonitorFactory, int logLevel, @NonNull RequestOptionsFactory defaultRequestOptionsFactory, @NonNull Map<Class<?>, TransitionOptions<?, ?>> defaultTransitionOptions, @NonNull List<RequestListener<Object>> defaultRequestListeners, boolean isLoggingRequestOriginsEnabled, boolean isImageDecoderEnabledForBitmaps) { this.engine = engine; this.bitmapPool = bitmapPool; this.arrayPool = arrayPool; this.memoryCache = memoryCache; this.requestManagerRetriever = requestManagerRetriever; this.connectivityMonitorFactory = connectivityMonitorFactory; this.defaultRequestOptionsFactory = defaultRequestOptionsFactory; final Resources resources = context.getResources(); //Registration machine, which maintains various registry information of encoding, decoding, loading, picture request header, supported pictures, etc registry = new Registry(); registry.register(new DefaultImageHeaderParser()); registry .append(int.class, InputStream.class, resourceLoaderStreamFactory) ....... ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory(); glideContext = new GlideContext(//Create a glideContext object context, arrayPool, registry, imageViewTargetFactory, defaultRequestOptionsFactory, defaultTransitionOptions, defaultRequestListeners, engine, isLoggingRequestOriginsEnabled, logLevel); }
In this process, some properties are assigned, and the GlideContext object and the registry object are created.
So far, our Glide singleton object creation is complete
Getting the RequestManger object
public static RequestManager with(@NonNull Context context) { return getRetriever(context).get(context); }
After getting the RequestManagerRetriever object, get the RequestManager object through the get method. Now let's trace the implementation of the code.
@NonNull public RequestManager get(@NonNull Context context) { if (context == null) {//Throw exception if context is empty throw new IllegalArgumentException("You cannot start a load on a null Context"); } else if (Util.isOnMainThread() && !(context instanceof Application)) { //If the current thread is the main thread and the context is not an Application, the corresponding life cycle is bound to the UI(Activity or Fragment), //Listen to the context lifecycle by creating a method to hide fragments. Then bind RequestManager and Fragment. //Because there are different ways to create fragments in v4 and ordinary fragments, different processing is carried out according to different context types if (context instanceof FragmentActivity) {//If it's FragmentActivity return get((FragmentActivity) context); } else if (context instanceof Activity) {//If it's a normal Activity return get((Activity) context); } else if (context instanceof ContextWrapper // Only unwrap a ContextWrapper if the baseContext has a non-null application context. // Context#createPackageContext may return a Context without an Application instance, // in which case a ContextWrapper may be used to attach one. && ((ContextWrapper) context).getBaseContext().getApplicationContext() != null) { return get(((ContextWrapper) context).getBaseContext()); } } //Return to application RequestManager return getApplicationManager(context); }
In the get() method, different processing is performed according to the type of Context. We trace a FragmentActivity type here, and others are similar
@NonNull public RequestManager get(@NonNull FragmentActivity activity) { if (Util.isOnBackgroundThread()) {//If it is a background thread, the context is processed according to the application, that is, it is unbound with the UI life cycle return get(activity.getApplicationContext()); } else { //Processing in UI thread assertNotDestroyed(activity);//Determine whether the activity has been destroyed FragmentManager fm = activity.getSupportFragmentManager(); return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity)); } } //Get RequestManager managed by FragmentManager private RequestManager supportFragmentGet(@NonNull Context context, @NonNull FragmentManager fm, @Nullable Fragment parentHint, boolean isParentVisible) { //Create an invisible SupportRequestManagerFragment to listen to the lifecycle of the corresponding Context SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm, parentHint, isParentVisible); //Get the corresponding request manager in the Fragment (each Fragment has only one unique request manager) RequestManager requestManager = current.getRequestManager(); if (requestManager == null) {//If the request manager is not currently set, create and set Glide glide = Glide.get(context); //Using the factory method, create a requestManager object requestManager = factory.build(glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context); current.setRequestManager(requestManager); } return requestManager; }
The actual request manager is built through factory
RequestManager(Glide glide, Lifecycle lifecycle, RequestManagerTreeNode treeNode, RequestTracker requestTracker, ConnectivityMonitorFactory factory, Context context) { this.glide = glide; this.lifecycle = lifecycle; this.treeNode = treeNode; this.requestTracker = requestTracker; this.context = context; //Connect monitor connectivityMonitor = factory .build(context.getApplicationContext(), new RequestManagerConnectivityListener(requestTracker)); if (Util.isOnBackgroundThread()) { mainHandler.post(addSelfToLifecycle); } else { lifecycle.addListener(this); } lifecycle.addListener(connectivityMonitor); defaultRequestListeners = new CopyOnWriteArrayList<>(glide.getGlideContext().getDefaultRequestListeners()); //Set request configuration information setRequestOptions(glide.getGlideContext().getDefaultRequestOptions()); //Register RequestManager to global glide glide.registerRequestManager(this); }
So far, we have created the RequestManager for, and then it calls the load() method.
load()
RequestManager supports image loading in various parameter forms:
We trace from our case, and the parameter passed in is of type String.
//It is equivalent to calling asDrawable() first and then calling the load() method. public RequestBuilder<Drawable> load(@RawRes @DrawableRes @Nullable Integer resourceId) { return asDrawable().load(resourceId); } public RequestBuilder<Drawable> asDrawable() { return as(Drawable.class); } //Create a RequestBuilder that can decode the corresponding type of pictures public <ResourceType> RequestBuilder<ResourceType> as( @NonNull Class<ResourceType> resourceClass) { return new RequestBuilder<>(glide, this, resourceClass, context); }
Let's see what the RequestBuilder's construction method does
protected RequestBuilder(@NonNull Glide glide, RequestManager requestManager, Class<TranscodeType> transcodeClass, Context context) { this.glide = glide; this.requestManager = requestManager; this.transcodeClass = transcodeClass; this.context = context; //The approximate rate of transcodeClass here is Drawable class object this.transitionOptions = requestManager.getDefaultTransitionOptions(transcodeClass); this.glideContext = glide.getGlideContext(); initRequestListeners(requestManager.getDefaultRequestListeners()); apply(requestManager.getDefaultRequestOptions()); }
After the RequestBuilder object is created, the load() method of RequestBuilder is called directly.
public RequestBuilder<TranscodeType> load(@Nullable String string) { return loadGeneric(string); } private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) { this.model = model; isModelSet = true; return this; }
There is no special operation here, just set the isModelSet to true, and the tag model has been set.
into()
In the with() and load() methods, we mainly do some preparatory work. The real operations of loading, caching and converting images to View are all performed in the into().
public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) { Util.assertMainThread();//Method needs to be executed on the main thread Preconditions.checkNotNull(view);//view cannot be empty BaseRequestOptions<?> requestOptions = this; if (!requestOptions.isTransformationSet() && requestOptions.isTransformationAllowed() && view.getScaleType() != null) {//Set the requestOptions according to the scaleType configured on the View switch (view.getScaleType()) { case CENTER_CROP: requestOptions = requestOptions.clone().optionalCenterCrop(); break; } } return into(glideContext.buildImageViewTarget(view, transcodeClass),/*targetListener=*/ null, requestOptions, Executors.mainThreadExecutor()); }
Here, we configure the requestOptions related parameters according to the information set, and then call the buildImageViewTarget method to construct a viewTarget object.
Let's take a look at this method
@NonNull public <X> ViewTarget<ImageView, X> buildImageViewTarget( @NonNull ImageView imageView, @NonNull Class<X> transcodeClass) { //The ViewTarget object is created by the factory method. Generally, DrawableImageViewTarget is returned here. If asBitmap is used, BitmapImageViewTarget is returned return imageViewTargetFactory.buildTarget(imageView, transcodeClass); } public class ImageViewTargetFactory { public <Z> ViewTarget<ImageView, Z> buildTarget( @NonNull ImageView view, @NonNull Class<Z> clazz) { if (Bitmap.class.equals(clazz)) {//If the asBitmap method is used, then the clazz here is bitmap return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view); } else if (Drawable.class.isAssignableFrom(clazz)) {//If it is only used normally, DrawableImageViewTarget will be returned return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view); } else { throw new IllegalArgumentException("Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)"); } } }
As you can see here, in most cases, a DrawableImageViewTarget object information is returned,
Back to the main line. Continue
private <Y extends Target<TranscodeType>> Y into(@NonNull Y target, @Nullable RequestListener<TranscodeType> targetListener, BaseRequestOptions<?> options, Executor callbackExecutor) { Preconditions.checkNotNull(target); if (!isModelSet) {//If the model is not set (a method that does not call the load method will cause this), an error is reported directly. throw new IllegalArgumentException("You must call #load() before calling #into()"); } //Create a request Request request = buildRequest(target, targetListener, options, callbackExecutor); //Get whether the corresponding request already exists on the target, and if so, clear it Request previous = target.getRequest(); if (request.isEquivalentTo(previous) && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) { //The request of the mark point on the target is the same as the generated request. Start the corresponding request directly, and then return the target object if (!Preconditions.checkNotNull(previous).isRunning()) { previous.begin(); } return target; } //Remove the original target from the request management class requestManager requestManager.clear(target); //Set new request to target target.setRequest(request); //Request manager enables tracking of target and request (mainly to start the request and manage the request class and target in a unified way) requestManager.track(target, request); return target; }
This code is the core of Glide, which creates and executes the Request object.
Creation process of request object
Let's first look at the creation process of Request object: buildRequest()
private Request buildRequest(Target<TranscodeType> target, @Nullable RequestListener<TranscodeType> targetListener, BaseRequestOptions<?> requestOptions, Executor callbackExecutor) { return buildRequestRecursive(/*requestLock=*/ new Object(), target, targetListener, /*parentCoordinator=*/ null, transitionOptions, requestOptions.getPriority(), requestOptions.getOverrideWidth(), requestOptions.getOverrideHeight(), requestOptions, callbackExecutor); } //Create a request class, which is divided into processing the request of error display and the request of normal display private Request buildRequestRecursive(Object requestLock, Target<TranscodeType> target, @Nullable RequestListener<TranscodeType> targetListener, @Nullable RequestCoordinator parentCoordinator, TransitionOptions<?, ? super TranscodeType> transitionOptions, Priority priority, int overrideWidth, int overrideHeight, BaseRequestOptions<?> requestOptions, Executor callbackExecutor) { ..... Request mainRequest =//Request for normal display buildThumbnailRequestRecursive(requestLock, target, targetListener, parentCoordinator, transitionOptions, priority, overrideWidth, overrideHeight, requestOptions, callbackExecutor); ... return errorRequestCoordinator; }
This function classifies the requests. One is the error displayed request information, and the other is the normal displayed request information.
Let's take a look at how it is created in the normally displayed request processing function.
//Generate and process the request of normal display, and create thumbnail or original image according to relevant settings private Request buildThumbnailRequestRecursive(Object requestLock,Target<TranscodeType> target, RequestListener<TranscodeType> targetListener,@Nullable RequestCoordinator parentCoordinator, TransitionOptions<?, ? super TranscodeType> transitionOptions,Priority priority, int overrideWidth,int overrideHeight,BaseRequestOptions<?> requestOptions,Executor callbackExecutor) { if (thumbnailBuilder != null) {//There is a thumbnail generator to create a thumbnail request based on builder information ... } else if (thumbSizeMultiplier != null) {//If there is image zoom index, create thumbnail request based on index information ... } else { //Generally, it will be handled in this way return obtainRequest(requestLock, target, targetListener, requestOptions, parentCoordinator, transitionOptions, priority, overrideWidth, overrideHeight, callbackExecutor); } }
This code segment is very long. We omitted it. Most of it is special processing for generating thumbnail request. We have a chance to analyze it later. Let's take a look at how the obtainRequest method creates standard requests.
private Request obtainRequest(Object requestLock, Target<TranscodeType> target, RequestListener<TranscodeType> targetListener, BaseRequestOptions<?> requestOptions, RequestCoordinator requestCoordinator, TransitionOptions<?, ? super TranscodeType> transitionOptions, Priority priority, int overrideWidth, int overrideHeight, Executor callbackExecutor) { return SingleRequest.obtain(context, glideContext, requestLock, model, transcodeClass, requestOptions, overrideWidth, overrideHeight, priority, target, targetListener, requestListeners, requestCoordinator, glideContext.getEngine(), transitionOptions.getTransitionFactory(), callbackExecutor); }
This method continues to trace. It can be found that only a SingleRequest object has been created and related parameters have been assigned. There is no special processing in it.
So far, our Request has been created. The next step is to see how network requests are made.
Execution of request
Back in the original into() code block, this time we are tracking the function requestManager.track(target, request)
synchronized void track(@NonNull Target<?> target, @NonNull Request request) { //Target management class, in which all target information is saved through set list, and the life cycle interface is implemented, which can start or pause target animation according to the life cycle targetTracker.track(target); //requestTracker management class, in which all the request information is saved by set list, the request is saved to set list by runRequest method, and the request is started requestTracker.runRequest(request); } //Start and track requests public void runRequest(@NonNull Request request) { requests.add(request);//Save to set list if (!isPaused) {//Load is available, call begin() directly request.begin(); } else {//When the load is in a suspended state and the request request is saved, it will be started one by one from the list when it is ready request.clear(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Paused, delaying request"); } //Put the request in pending requests, because requests are weak references, preventing them from being recycled pendingRequests.add(request); } }
This is a simple process. If the current RequestManager is available (that is, the bound Context is visible), the task request processing will be performed directly. Otherwise, the request will be placed in the set list corresponding to pendingRequests. Although the corresponding request information has been saved in requests, it will be recycled because it is a weak reference, so pendingRequests is used for saving.
In the analysis of creating the request object, we know that the last one created is the SingleRequest object, and continue to trace the begin() method
public void begin() { synchronized (requestLock) { assertNotCallingCallbacks(); stateVerifier.throwIfRecycled(); startTime = LogTime.getLogTime(); if (model == null) {//If the loaded image source (url,file, etc.) is null, onLoadFailed will be used directly ... onLoadFailed(new GlideException("Received null model"), logLevel); return; } if (status == Status.RUNNING) {//If the request is in progress, throw the exception directly throw new IllegalArgumentException("Cannot restart a running request"); } if (status == Status.COMPLETE) {//If the request has been completed, call the onResourceReady interface directly //If we do it after a reboot (usually through a notifyDataSetChanged to start the same request to the same target or view), //We can simply use resources and sizes without having to get a new size, start a new load, etc. // This does mean that if the customer does need to reload, the displayed call clear clears the view or target before loading. onResourceReady(resource, DataSource.MEMORY_CACHE); return; } status = Status.WAITING_FOR_SIZE; if (Util.isValidDimensions(overrideWidth, overrideHeight)) { //If the width and height of the view to be loaded are fixed, load it directly onSizeReady(overrideWidth, overrideHeight); } else { //Set the callback information. When the width and height of the View are finished, the callback processing is performed. Through the getViewTreeObserver method of target(Target class, which contains the information of View), monitor the drawing of the control, so as to obtain the corresponding width and height. Finally, call the onSizeReady(width,height) method through the interface callback target.getSize(this); } if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE) && canNotifyStatusChanged()) { //Set the PlaceholderDrawable information, load the resources in the background, and display the corresponding data on the target target.onLoadStarted(getPlaceholderDrawable()); } if (IS_VERBOSE_LOGGABLE) { logV("finished run method in " + LogTime.getElapsedMillis(startTime)); } } }
In the begin() function, the main function is to call the onSizeReady method to perform the specific loading process when the View width and height are known (whether fixed or through the drawn callback). In addition, when the loading is not completed, the corresponding bitmap is set on the View (that is, the. placeholder() method that we often use in the code).
public void onSizeReady(int width, int height) { stateVerifier.throwIfRecycled(); synchronized (requestLock) { ... float sizeMultiplier = requestOptions.getSizeMultiplier();//Width height reprocessing according to scale this.width = maybeApplySizeMultiplier(width, sizeMultiplier); this.height = maybeApplySizeMultiplier(height, sizeMultiplier); ... loadStatus = engine.load(//To load glideContext, model, requestOptions.getSignature(), this.width, this.height, requestOptions.getResourceClass(), transcodeClass, priority, requestOptions.getDiskCacheStrategy(), requestOptions.getTransformations(), requestOptions.isTransformationRequired(), requestOptions.isScaleOnlyOrNoTransform(), requestOptions.getOptions(), requestOptions.isMemoryCacheable(), requestOptions.getUseUnlimitedSourceGeneratorsPool(), requestOptions.getUseAnimationPool(), requestOptions.getOnlyRetrieveFromCache(), this, callbackExecutor); ... } }
The function finally performs the loading process by calling engine.load.
//Real load, processed through multi-level cache public <R> LoadStatus load( GlideContext glideContext, Object model, Key signature, int width, int height, Class<?> resourceClass, Class<R> transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map<Class<?>, Transformation<?>> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, Options options, boolean isMemoryCacheable, boolean useUnlimitedSourceExecutorPool, boolean useAnimationPool, boolean onlyRetrieveFromCache, ResourceCallback cb, Executor callbackExecutor) { long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0; EngineKey key = keyFactory.buildKey(//Create a key value according to the relevant parameters model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource<?> memoryResource; synchronized (this) { //Load from memory memoryResource = loadFromMemory(key, isMemoryCacheable, startTime); if (memoryResource == null) { //If there is no query in memory, create a Job internally and execute it, return LoadStatus return waitForExistingOrStartNewJob( glideContext, model, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, options, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache, cb, callbackExecutor, key, startTime); } }
As we all know, any existing image loading framework uses cache to process data, so as to speed up image loading. Glide is no exception. You can see the loading of resources from the code.
A key is generated through related parameters
- Search in memory through key
- If it is found, the resource information is returned directly through the interface callback.
- If not, a new task request is created to load.
Let's start with the memory loading function loadFromMemory() to analyze how it is handled.
private EngineResource<?> loadFromMemory(EngineKey key, boolean isMemoryCacheable, long startTime) { if (!isMemoryCacheable) {//If it is not allowed to load in the cache, directly return to turn off the cache function through. skipMemoryCache(false) return null; } //Get from the list of pictures in use EngineResource<?> active = loadFromActiveResources(key); if (active != null) { return active; } //Get from memorycache (usually lurmemorycache is used here) EngineResource<?> cached = loadFromCache(key); if (cached != null) { return cached; } return null; } //Loaded from resources in use private EngineResource<?> loadFromActiveResources(Key key) { //Get from loaded resources EngineResource<?> active = activeResources.get(key); if (active != null) {//Resource consuming counter + 1 active.acquire(); } return active; } //Get from cache private EngineResource<?> loadFromCache(Key key) { //Get from Cache EngineResource<?> cached = getEngineResourceFromCache(key); if (cached != null) { //If it exists in the cache, it is removed from the cache and placed in the list of resources being used cached.acquire(); activeResources.activate(key, cached); } return cached; }
The source code is relatively simple.
- loadFromActiveResources() first looks up the resources in use. If found, the resource's reference counter + 1 is returned to the resource file.
- If it is not found, it is obtained from the cache, and the lrumemorycache used by the cache by default (can be used). If successful, it is removed from the cache and placed in active resources (strong reference). Prevent resource invalidation caused by cache being recycled.
- If not, the entire function returns null
If the resource file is not retrieved from memory, the code will load the resource through waitForExistingOrStartNewJob.
//Use existing jobs or create new jobs to load resources private <R> LoadStatus waitForExistingOrStartNewJob(...) { //From the list of jobs executed, onlyRetrieveFromCache is to load picture flag bits from cache only. EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache); if (current != null) { //If it exists, add a callback interface for the current task, //It is possible that a resource is used in multiple places at the same time. Therefore, after loading, multiple interfaces need to be called back for notification current.addCallback(cb, callbackExecutor); return new LoadStatus(cb, current); } //Create an EngineJob EngineJob<R> engineJob =engineJobFactory.build(...); //It is responsible for obtaining data from cached data resources or original resources. It is a data acquisition class DecodeJob<R> decodeJob =decodeJobFactory.build(...); //Store the engineJob in jobs. There is a field of onlyRetrieveFromCache in engineJob, so it can be placed in different list s according to the field jobs.put(key, engineJob); //Add callback interface engineJob.addCallback(cb, callbackExecutor); //Start engineJob to load resources engineJob.start(decodeJob); return new LoadStatus(cb, engineJob); }
We are only concerned about the process of creating a new EngineJob and executing the load, so here we mainly look at the execution of engineJob.start(decodeJob)
public synchronized void start(DecodeJob<R> decodeJob) { this.decodeJob = decodeJob; //According to the setting of decodeJob, use the non saved actuator GlideExecutor executor = decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor(); //Start the decodeJob, which implements the Runnable interface and calls its run() method executor.execute(decodeJob); }
Let's now look at the run method of DecodeJob
@Override public void run() { DataFetcher<?> localFetcher = currentFetcher; try { if (isCancelled) {//If it has been cancelled, it will directly return to failure. isCancelled is volatile. Ensure the visibility of multithreading notifyFailed();//Inform the upper layer (EngineJob) that the call fails, and recycle the related resources return; } //Main load function runWrapped(); } catch (CallbackException e) { //If we have already called the callback upper interface when we have entered the encode phase, we need to release resources by calling the failed interface. Otherwise, it is not safe // You can check the notifyEncodeAndRelease(Resource, DataSource) method if (stage != Stage.ENCODE) { throwables.add(t); notifyFailed(); } throw t; } finally { if (localFetcher != null) {//Clean up the localFetcher because the DecodeJob is reused. localFetcher.cleanup(); } GlideTrace.endSection();//Record tracking information of the entire Glide } }
The run method has only one main function, runWrapped().
private void runWrapped() { switch (runReason) { case INITIALIZE://If it is the initialization phase stage = getNextStage(Stage.INITIALIZE); currentGenerator = getNextGenerator(); runGenerators(); break; case SWITCH_TO_SOURCE_SERVICE: runGenerators(); break; case DECODE_DATA: decodeFromRetrievedData(); break; default: throw new IllegalStateException("Unrecognized run reason: " + runReason); } } //Data resource acquisition generator, initialization - > resource cache decoding - > data cache - > source - > end // According to different stages, different data loaders are generated private DataFetcherGenerator getNextGenerator() { switch (stage) { case RESOURCE_CACHE://Load data from cache file (including resources after down sampling / conversion) return new ResourceCacheGenerator(decodeHelper, this); case DATA_CACHE://Data cache load data (original cache resource) return new DataCacheGenerator(decodeHelper, this); case SOURCE://Source address load data of resource return new SourceGenerator(decodeHelper, this); case FINISHED: return null; default: throw new IllegalStateException("Unrecognized stage: " + stage); } } /** * Returns to the next state, based on the current state. Because diskCacheStrategy uses AUTOMATIC's caching policy by default, * decodeCachedResource()And decodeCachedData() return true * Next phase diagram: * Initialization - > resource cache decoding - > data cache - > source - > end * */ private Stage getNextStage(Stage current) { switch (current) { case INITIALIZE: return diskCacheStrategy.decodeCachedResource()? Stage.RESOURCE_CACHE: getNextStage(Stage.RESOURCE_CACHE); case RESOURCE_CACHE: return diskCacheStrategy.decodeCachedData()? Stage.DATA_CACHE: getNextStage(Stage.DATA_CACHE); case DATA_CACHE: return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE; case SOURCE: case FINISHED: return Stage.FINISHED; default: throw new IllegalArgumentException("Unrecognized stage: " + current); } }
The main processes of Glide's resource loading are as follows
- Resource cache decoding
- Data cache
- Data source
The corresponding data loading classes are:
- ResourceCacheGenerator: load data from cache file (including resources after down sampling / conversion)
- DataCacheGenerator: load data from data cache (original cache resource)
- SourceGenerator: load data from the source address of the resource
Depending on the user's actual configuration information, one or more steps in the middle may be skipped.
After creating the data loading class, the relevant data loading is started through the runGenerators() * method.
private void runGenerators() { currentThread = Thread.currentThread(); startFetchTime = LogTime.getLogTime(); boolean isStarted = false; //Stop cycle condition: canceled, current data loader is not empty, and current loader is not loaded to related resources while (!isCancelled && currentGenerator != null && !(isStarted = currentGenerator.startNext())) { stage = getNextStage(stage); currentGenerator = getNextGenerator(); if (stage == Stage.SOURCE) {//If it is obtained from the resource source, enter the relevant scheduling of the source acquisition reschedule(); return; } } if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {//If it is finished or cancelled, the direct callback fails notifyFailed(); } }
In this function, the relevant data loader is traversed and executed through the while loop until all data loaders have been executed or some loader has loaded the relevant data.
startNextLet's first look at how the ResourceCacheGenerator performs the load process.
public boolean startNext() { //Each model corresponds to multiple parsers. Finally, according to the format of the model (String,Uri, etc.), find the LoadData list that can be used. Each LoadData will have a corresponding key. //The corresponding key list is obtained here List<Key> sourceIds = helper.getCacheKeys();//If the model does not have a corresponding mapped key. Then return false directly if (sourceIds.isEmpty()) { return false; } //Get the resource class information that can be decode d from model to resource List<Class<?>> resourceClasses = helper.getRegisteredResourceClasses(); if (resourceClasses.isEmpty()) { if (File.class.equals(helper.getTranscodeClass())) { return false; } throw new IllegalStateException( "Failed to find any load path from " + helper.getModelClass() + " to " + helper.getTranscodeClass()); } while (modelLoaders == null || !hasNextModelLoader()) {//Double traversal resourceClassIndex++; if (resourceClassIndex >= resourceClasses.size()) { sourceIdIndex++; if (sourceIdIndex >= sourceIds.size()) { return false; } resourceClassIndex = 0; } Key sourceId = sourceIds.get(sourceIdIndex); Class<?> resourceClass = resourceClasses.get(resourceClassIndex); Transformation<?> transformation = helper.getTransformation(resourceClass);se. currentKey = new ResourceCacheKey( // NOPMD Avoid Instantiating Objects InLoops helper.getArrayPool(), sourceId, helper.getSignature(), helper.getWidth(), helper.getHeight(), transformation, resourceClass, helper.getOptions()); //Get the cached file according to the current key cacheFile = helper.getDiskCache().get(currentKey); if (cacheFile != null) {//Get the cache file, set the corresponding modelLoader and key, and it will jump out of the loop sourceKey = sourceId; modelLoaders = helper.getModelLoaders(cacheFile); modelLoaderIndex = 0; } } loadData = null; boolean started = false; while (!started && hasNextModelLoader()) { ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++); loadData = modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions()); if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) { //There is corresponding LoadData started = true; //Load the corresponding file through the fetcher loadData.fetcher.loadData(helper.getPriority(), this); } } return started; }
Let's summarize the general process:
1. Obtain the corresponding sourceIds and resourceClasses according to the parameters passed in
2. Orthogonal traversal, iterating each group. Generate the key used in the cache file according to the relevant parameters, and then query whether there is a cache file according to the key
3. If the corresponding cache file is found, set sourceKey and modelLoaders. You can then jump out of the loop with these two settings.
4. modeLoaders traverse, get modelLoader and create corresponding loadData.
5. If the LoadData exists and the data class obtained by its internal datafetcher (data reader) exists, set the started flag bit to indicate that it has been loaded into the relevant data from ResourceCacheGenerator (so that all subsequent loaders will no longer execute). Then get the data through datafetcher.
6. If the traverse does not load relevant data, the returned started flag bit is false. Indicates that the ResourceCacheGenerator is not loaded into the related resource. After that, the loader (datacache generator, or SourceGenerator, or out of loop, depending on the previous settings) will continue to execute.
If it is loaded for the first time, the resource cannot be obtained in the DataCacheGenerator. Then the next one will execute the startNext() method of the DataCacheGenerator
startNext@Override public boolean startNext() { while (modelLoaders == null || !hasNextModelLoader()) { sourceIdIndex++; if (sourceIdIndex >= cacheKeys.size()) { return false; } //Get Key information of source resource Key sourceId = cacheKeys.get(sourceIdIndex);//cacheKeys=helper.getCacheKeys() //Obtain the corresponding original Key according to the current sourceId. Key originalKey = new DataCacheKey(sourceId, helper.getSignature()); //Get the cache file from the disk cache according to the original Key, and jump out of the loop cacheFile = helper.getDiskCache().get(originalKey); if (cacheFile != null) { this.sourceKey = sourceId; modelLoaders = helper.getModelLoaders(cacheFile); modelLoaderIndex = 0; } } loadData = null; boolean started = false; while (!started && hasNextModelLoader()) { ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++); loadData = modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions()); if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) { //There is corresponding LoadData started = true; //Load the corresponding file through the fetcher loadData.fetcher.loadData(helper.getPriority(), this); } } return started; }
The main purpose of DataCacheGenerator is to get the original cache file. You can see that the general process is similar to ResourceCacheGenerator. The only difference is that the Key obtained is different. When obtaining the cache file, the parameter used is DataCacheKey, which is the Key value of the original cache file.
When loading for the first time, this must not be retrieved, and the returned one is empty. At this time, the next one will execute to the startNext() method of SourceGenerator.
startNextpublic boolean startNext() { if (dataToCache != null) { //After the first resource loading, a thread switch will be performed, and this method will be called again. At this time, dataToCache is not empty, and data will be saved. This is the source file of disk cache // And generated the DataCacheGenerator class Object data = dataToCache; dataToCache = null; cacheData(data); } if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) { //In the case of a DataCacheGenerator class, the startNext method is called to load the file from the source file return true; } sourceCacheGenerator = null; loadData = null; boolean started = false; while (!started && hasNextModelLoader()) { //Cycle to get all the modelLoaders related to the current model and get the loadData data loadData = helper.getLoadData().get(loadDataListIndex++); //It should be noted here that if the cache policy thinks that data can be cached, then you do not need to manage the loadPath behind, and get the data first. //Since the cache will be handed over to the DataCacheGenerator for processing, you can skip the hasLoadPath judgment later. //hasLoadPath is based on the registered decoder in the Registry, and the converter judges whether the transformation of data class - > resource class - > transcodeclass can be completed. if (loadData != null && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource()) || helper.hasLoadPath(loadData.fetcher.getDataClass()))) { started = true; startNextLoad(loadData); } } return started; } private void startNextLoad(final LoadData<?> toStart) { loadData.fetcher.loadData(//Load resources helper.getPriority(), new DataCallback<Object>() { @Override public void onDataReady(@Nullable Object data) { if (isCurrentRequest(toStart)) { onDataReadyInternal(toStart, data); } } @Override public void onLoadFailed(@NonNull Exception e) { if (isCurrentRequest(toStart)) { onLoadFailedInternal(toStart, e); } } }); }
When the resource loading is completed, the onDataReady interface will be called back. Let's take a look at the implementation of onDataReadyInternal.
@Synthetic void onDataReadyInternal(LoadData<?> loadData, Object data) { DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy(); if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) { //If the DataSource corresponding to DataFetcher can be cached dataToCache = data; //Here, cb refers to the DecodeJob class - > callback.reschedule (this), and callback refers to the EngineJob class //->getActiveSourceExecutor().execute(job); //That is, through the thread pool, the EngineJob is executed again, and then the startNext method of this class will be executed. Because dataToCache is not empty, the code block inside will be executed cb.reschedule(); } else { //If caching is not possible, the onDataFetcherReady method is called directly cb.onDataFetcherReady( loadData.sourceKey, data, loadData.fetcher, loadData.fetcher.getDataSource(), originalKey); } }
Let's first analyze the first situation.
We wrote the comments in detail, and we know that we will call the startNext method of this class again in the end. Let's take a look at the beginning of this method.
if (dataToCache != null) { //After the first resource loading, a thread switch will be performed, and this method will be called again. At this time, dataToCache is not empty, and data will be saved. This is the source file of disk cache // And generated the DataCacheGenerator class Object data = dataToCache; dataToCache = null; cacheData(data); } if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) { //In the case of a DataCacheGenerator class, the startNext method is called to load the file from the source file return true; } private void cacheData(Object dataToCache) { long startTime = LogTime.getLogTime(); try { //Get the encoder, through traversing the encoder list in the registry, get the encoder of the corresponding cache class Encoder<Object> encoder = helper.getSourceEncoder(dataToCache); //generate DataCacheWriter<Object> writer = new DataCacheWriter<>(encoder, dataToCache, helper.getOptions()); //Generate the key of the original cache file for encoding lookup in the DataCacheGenerator originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature()); //Data caching through DiskCache helper.getDiskCache().put(originalKey, writer); } finally { loadData.fetcher.cleanup(); } sourceCacheGenerator = new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this); }
That is, first save the original file on the disk, and then load it through the DataCacheGenerator class.
Now let's look at the second situation:
public void onDataFetcherReady( Key sourceKey, Object data, DataFetcher<?> fetcher, DataSource dataSource, Key attemptedKey) { ... if (Thread.currentThread() != currentThread) { //When you set the execution reason to decode data and call unwrapper again, you can directly execute the decoding phase. This is the decodeFromRetrievedData method in else runReason = RunReason.DECODE_DATA; callback.reschedule(this); } else { GlideTrace.beginSection("DecodeJob.decodeFromRetrievedData"); try { decodeFromRetrievedData(); } finally { GlideTrace.endSection(); } } }
At this time, a new call will be made according to the thread, or the decodeFromRetrievedData() method will be called directly.
//Decoding retrieved data private void decodeFromRetrievedData() { if (Log.isLoggable(TAG, Log.VERBOSE)) { logWithTimeAndKey( "Retrieved data", startFetchTime, "data: " + currentData + ", cache key: " + currentSourceKey + ", fetcher: " + currentFetcher); } Resource<R> resource = null; try { //Decoding resources resource = decodeFromData(currentFetcher, currentData, currentDataSource); } catch (GlideException e) { e.setLoggingDetails(currentAttemptingKey, currentDataSource); throwables.add(e); } if (resource != null) { //Through the interface callback, the resource is loaded successfully, and then the layer by layer callback is performed in decodejon - > enginejob - > Target. Finally, the Target operates ImageView to render the resource to View notifyEncodeAndRelease(resource, currentDataSource); } else { runGenerators(); } }
The general process is basically completed. The details will be added a little bit later.
Add several documents:
Modelloader (resource loading class): contains two methods
buildLoadData(): create LoadData class
handles(Model): whether the class can load the given model
LoadData class: contains 3 properties
sourceKey: resource key
Alternate keys: list of temporary keys
fetcher: an instance of DataFetcher interface, which defines the loadData method to get data
DataFetcher interface: real resource acquisition
loadData(): get the data that can be decoded
cleanup(): empty or recycle resources
getDataClass(): the class of the resource that the current implementation class can obtain
getDataSource(): from which data source will this fetcher return data, enumeration type.
This article is composed of Ken opened. Release!