Public number: byte array, hope to help you 😇😇
For Android Developer, many open source libraries are essential knowledge points for development, from the use of ways to implementation principles to source code parsing, which require us to have a certain degree of understanding and application ability. So I’m going to write a series of articles about source code analysis and practice of open source libraries, the initial target is EventBus, ARouter, LeakCanary, Retrofit, Glide, OkHttp, Coil and other seven well-known open source libraries, hope to help you 😇😇
Article Series Navigation:
- Tripartite library source notes (1) -EventBus source detailed explanation
- Tripartite library source notes (2) -EventBus itself to implement one
- Three party library source notes (3) -ARouter source detailed explanation
- Third party library source notes (4) -ARouter own implementation
- Three database source notes (5) -LeakCanary source detailed explanation
- Tripartite Library source note (6) -LeakCanary Read on
- Tripartite library source notes (7) -Retrofit source detailed explanation
- Tripartite library source notes (8) -Retrofit in combination with LiveData
- Three party library source notes (9) -Glide source detailed explanation
- Tripartite library source notes (10) -Glide you may not know the knowledge point
- Three party library source notes (11) -OkHttp source details
- Tripartite library source notes (12) -OkHttp/Retrofit development debugger
- Third party library source notes (13) – may be the first network Coil source analysis article
Glide source code is a little complex, if you want to elaborate to explain, then write a ten articles also include not complete 😂😂 so I think a different way of thinking to see the source: To small points to divide, each small point only contains Glide to achieve a function or purpose involved in the process, in order to simplify the difficulty of understanding, through the integration of a number of small function points to control the Glide big implementation direction
This article is based on the latest version of Glide currently available
dependencies {
implementation 'com. Making. Bumptech. Glide: glide: 4.11.0'
kapt 'com. Making. Bumptech. Glide: the compiler: 4.11.0'
}
Copy the code
An overview,
Before you start looking at Glide source code, you need to have some basic understanding of Glide
Glide’s cache mechanism is divided into memory cache and disk cache two levels. By default, Glide will automatically cache the loaded images, which can be divided into memory cache and disk cache, and the cache logic adopts LruCache algorithm. By default, Glide values a network image in the following order:
- When launching a request to load an image, it first checks to see if there is a matching image in ActiveResources. If so, it takes the value directly, otherwise it goes to the next step. ActiveResources is used to store in memory the image resource that is currently in use (for example, an ImageView is displaying the image), and ActiveResources holds a reference to the image through a weak reference
- Check whether the MemoryCache contains a matching image. If so, take the value directly, otherwise go to the next step. MemoryCache uses the Lru algorithm to cache in memory image resources that have been used but are not currently in use
- Check whether there are images matching the conditions in DiskCache. If yes, decode the value. Otherwise, go to the next step. DiskCache also uses the Lru algorithm to cache images that have been loaded on the local disk
- Networking request image. After the image is loaded, the image is cached to disk and memory, namely DiskCache, ActiveResources, and MemoryCache, for subsequent reuse
So Glide’s MemoryCache is divided into ActiveResources and MemoryCache
In addition, Glide will eventually be cached to disk can be divided into two types of images, one is the original image, one is the original image after various compression, cropping, transformation and other conversion operations. Glide’s DiskCacheStrategy is divided into the following five categories to determine how to save these two types of images on disk
Disk caching Policy | Cache Policy |
---|---|
DiskCacheStrategy.NONE | Nothing is cached |
DiskCacheStrategy.ALL | Cache both the original image and the converted image |
DiskCacheStrategy.DATA | Only the original image is cached |
DiskCacheStrategy.RESOURCE | Only the converted images are cached |
DiskCacheStrategy.AUTOMATIC | Glide automatically selects which caching policy to use based on the image resource type (default option) |
, a special caching policy is DiskCacheStrategy. The AUTOMATIC, the strategy will be depending on the type of image to be loaded using the best caching strategies. If you load remote images, only the original images are stored, not the converted images, because downloading remote images is much more expensive than adjusting data already on the disk. If you load a local image, only the converted image is stored, because even if you need to generate another image of another size or type, it’s easy to get back the original image
Since disk space is limited, AUTOMATIC is a middle-of-the-road choice that measures both the size of the disk space and the cost of obtaining images
How to monitor the life cycle
The ImageView is mounted on the Activity or Fragment container. When the container is in the background or has been finished, the loading of the image should be cancelled or stopped. Otherwise, you are wasting valuable system and network resources, and may even have memory leaks or NPE problems. The obvious question is, how does Glide determine whether a container is still active?
Similar to the implementation of Lifecycle in the Jetpack component, Glide indirectly obtains the Lifecycle state of the container through a UI-less Fragment. Lifecycle implementation ideas can be found in my source code explanation article:Jetpack (1) -Lifecycle source parsing from source
Glide implementation lifecycle listening involves the following classes:
- LifecycleListener
- Lifecycle
- ActivityFragmentLifecycle
- ApplicationLifecycle
- SupportRequestManagerFragment
First, LifecycleListener defines three event notification callbacks to notify the container of its active state (foreground, background, or exit). Lifecycle is used to register and remove the LifecycleListener
public interface LifecycleListener {
void onStart(a);
void onStop(a);
void onDestroy(a);
}
public interface Lifecycle {
void addListener(@NonNull LifecycleListener listener);
void removeListener(@NonNull LifecycleListener listener);
}
Copy the code
For a container instance, for example, during the lifetime of an Activity, the Activity may load more than one image successively. Accordingly, multiple background tasks to load the image need to be started successively, and each background task needs to be notified when the Activity’s lifecycle state changes. This entire notice process corresponding ActivityFragmentLifecycle this class
ActivityFragmentLifecycle isStarted and isDestroyed two Boolean variables to mark the Activity of the current active state, and provides the ability to save and inform more LifecycleListener
class ActivityFragmentLifecycle implements Lifecycle {
private final Set<LifecycleListener> lifecycleListeners =
Collections.newSetFromMap(new WeakHashMap<LifecycleListener, Boolean>());
private boolean isStarted;
private boolean isDestroyed;
@Override
public void addListener(@NonNull LifecycleListener listener) {
lifecycleListeners.add(listener);
if (isDestroyed) {
listener.onDestroy();
} else if (isStarted) {
listener.onStart();
} else{ listener.onStop(); }}@Override
public void removeListener(@NonNull LifecycleListener listener) {
lifecycleListeners.remove(listener);
}
void onStart(a) {
isStarted = true;
for(LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onStart(); }}void onStop(a) {
isStarted = false;
for(LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onStop(); }}void onDestroy(a) {
isDestroyed = true;
for(LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onDestroy(); }}}Copy the code
ActivityFragmentLifecycle used in SupportRequestManagerFragment the fragments to use (omitted part of the code). As you can see, in the three fragments lifecycle callback event, will inform ActivityFragmentLifecycle accordingly. So, whether or not the ImageView is the carrier of the Activity or fragments, we can to inject a UI SupportRequestManagerFragment, to monitor the carrier changes in the active state in the entire life cycle
public class SupportRequestManagerFragment extends Fragment {
private static final String TAG = "SupportRMFragment";
private final ActivityFragmentLifecycle lifecycle;
public SupportRequestManagerFragment(a) {
this(new ActivityFragmentLifecycle());
}
@VisibleForTesting
@SuppressLint("ValidFragment")
public SupportRequestManagerFragment(@NonNull ActivityFragmentLifecycle lifecycle) {
this.lifecycle = lifecycle;
}
@NonNull
ActivityFragmentLifecycle getGlideLifecycle(a) {
return lifecycle;
}
@Override
public void onStart(a) {
super.onStart();
lifecycle.onStart();
}
@Override
public void onStop(a) {
super.onStop();
lifecycle.onStop();
}
@Override
public void onDestroy(a) {
super.onDestroy();
lifecycle.onDestroy();
unregisterFragmentWithRoot();
}
@Override
public String toString(a) {
return super.toString() + "{parent=" + getParentFragmentUsingHint() + "}"; }}Copy the code
Glide cannot do Lifecycle listening in two special cases where the Lifecycle implementation class is ApplicationLifecycle and is always onStart by default:
- The Context passed to Glide is of the Application type. Applications do not have life cycle events in the usual sense
- Load the image in the child thread. At this point the developer may want to get to the Bitmap object directly
class ApplicationLifecycle implements Lifecycle {
@Override
public void addListener(@NonNull LifecycleListener listener) {
listener.onStart();
}
@Override
public void removeListener(@NonNull LifecycleListener listener) {
// Do nothing.}}Copy the code
3. How to inject fragments
Now know Glide through SupportRequestManagerFragment got life cycle events, So how does SupportRequestManagerFragment mounted to the Activity or fragments?
By looking for reference, to locate is done in RequestManagerRetriever getSupportRequestManagerFragment method SupportRequestManagerFragment injection
public class RequestManagerRetriever implements Handler.Callback {
@NonNull
private SupportRequestManagerFragment getSupportRequestManagerFragment(
@NonNull final FragmentManager fm, @Nullable Fragment parentHint, boolean isParentVisible) {
/ / by the TAG whether in FragmentManager already contains SupportRequestManagerFragment
SupportRequestManagerFragment current =
(SupportRequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
if (current == null) {
/ / the current SupportRequestManagerFragment null shows that hasn't been injected
/ / then build a SupportRequestManagerFragment instance and added to the FragmentManager
current = pendingSupportRequestManagerFragments.get(fm);
if (current == null) {
current = new SupportRequestManagerFragment();
current.setParentFragmentHint(parentHint);
if(isParentVisible) { current.getGlideLifecycle().onStart(); } pendingSupportRequestManagerFragments.put(fm, current); fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss(); handler.obtainMessage(ID_REMOVE_SUPPORT_FRAGMENT_MANAGER, fm).sendToTarget(); }}returncurrent; }}Copy the code
What is the exact timing of the injection?
When we use Glide to load an image, it’s often something as unpretentious as the one shown below, a single line of code, with tons of work behind the scenes
Glide.with(FragmentActivity).load(url).into(ImageView)
Copy the code
When you call Glide. With (FragmentActivity), the final call will be to the RequestManagerRetriever’s get(FragmentActivity) method, In internal call supportFragmentGet method complete SupportRequestManagerFragment injection, and eventually return to a RequestManager object, The RequestManager stores all image loading tasks started with this FragmentActivity
public class RequestManagerRetriever implements Handler.Callback {
@NonNull
public RequestManager get(@NonNull FragmentActivity activity) {
if (Util.isOnBackgroundThread()) {
// For background threads, then ApplicationLifecycle is used
return get(activity.getApplicationContext());
} else {
assertNotDestroyed(activity);
FragmentManager fm = activity.getSupportFragmentManager();
return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity)); }}@NonNull
private RequestManager supportFragmentGet(
@NonNull Context context,
@NonNull FragmentManager fm,
@Nullable Fragment parentHint,
boolean isParentVisible) {
/ / finish SupportRequestManagerFragment injection operation here
SupportRequestManagerFragment current =
getSupportRequestManagerFragment(fm, parentHint, isParentVisible);
RequestManager requestManager = current.getRequestManager();
if (requestManager == null) {
// TODO(b/27524013): Factor out this Glide.get() call.
Glide glide = Glide.get(context);
requestManager =
factory.build(
glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
current.setRequestManager(requestManager);
}
returnrequestManager; }}Copy the code
So, when we call Glide. With (FragmentActivity) method, where you have already completed the SupportRequestManagerFragment injection
The RequestManagerRetriever has a GET method overload that contains several inbound parameter types
- Context
- androidx.fragment.app.FragmentActivity
- android.app.Activity
- androidx.fragment.app.Fragment
- Android.app. Fragment (deprecated)
- View
The logic of these GET methods can be summarized as:
- If the external thread is called through a child thread, then use the Application thread. In this case, you do not need to inject the Fragment, and use the ApplicationLifecycle directly. By default, the external thread is always active
- If Application is passed in from the outside, the steps are the same as above
- If the incoming View is not associated with the Activity (for example, the View contains a Context of type ServiceContext), then the steps are the same as above
- In addition to the above circumstance, can eventually through external to the incoming parameters associated to find the Activity or fragments, and eventually to inject RequestManagerFragment or SupportRequestManagerFragment
RequestManagerFragment function and SupportRequestManagerFragment, but is, here is no longer here
For example, the get (@ NonNull Context Context) will be based on the caller’s thread type and Context belongs to determine how to inject SupportRequestManagerFragment, resulting in different RequestManager. If no injection SupportRequestManagerFragment, then use RequestManager objects belong to the Application level of RequestManager globally unique
/** The top application level RequestManager. */
private volatile RequestManager applicationManager;
@NonNull
public RequestManager get(@NonNull Context context) {
if (context == null) {
throw new IllegalArgumentException("You cannot start a load on a null Context");
} else if(Util.isOnMainThread() && ! (contextinstanceof Application)) {
// Call on the main thread where context is not Application
if (context instanceof FragmentActivity) {
return get((FragmentActivity) context);
} else if (context instanceof 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) {
returnget(((ContextWrapper) context).getBaseContext()); }}// Call in the child thread or context is Application
return getApplicationManager(context);
}
@NonNull
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) {
// Normally pause/resume is taken care of by the fragment we add to the fragment or
// activity. However, in this case since the manager attached to the application will not
// receive lifecycle events, we must force the manager to start resumed using
// ApplicationLifecycle.
// TODO(b/27524013): Factor out this Glide.get() call.
Glide glide = Glide.get(context.getApplicationContext());
applicationManager =
factory.build(
glide,
new ApplicationLifecycle(),
newEmptyRequestManagerTreeNode(), context.getApplicationContext()); }}}return applicationManager;
}
Copy the code
4. How to start the task of loading images
We’ve seen how Glide monitors Activity lifecycle changes. How does Glide initiate the task of loading images?
Mentioned above, when we call the Glide. With (FragmentActivity), will complete SupportRequestManagerFragment injection operation. For the same Activity instance, it is injected only once during its single lifecycle. From supportFragmentGet method can also see that every SupportRequestManagerFragment contains a RequestManager instance
public class RequestManagerRetriever implements Handler.Callback {
@NonNull
private RequestManager supportFragmentGet(
@NonNull Context context,
@NonNull FragmentManager fm,
@Nullable Fragment parentHint,
boolean isParentVisible) {
/ / finish SupportRequestManagerFragment injection operation here
SupportRequestManagerFragment current =
getSupportRequestManagerFragment(fm, parentHint, isParentVisible);
RequestManager requestManager = current.getRequestManager();
if (requestManager == null) {
/ / if requestManager null will be generated and set into the SupportRequestManagerFragment
// TODO(b/27524013): Factor out this Glide.get() call.
Glide glide = Glide.get(context);
requestManager =
factory.build(
glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
current.setRequestManager(requestManager);
}
returnrequestManager; }}Copy the code
The RequestManager class is where you start and manage all image-loading tasks before and after an Activity starts. When we fully call the Into method Glide. With (FragmentActivity).load(URL).into(ImageView), we build a Request object that represents the currently loaded task. And pass the task to the RequestManager to start tracking the task
@NonNull
public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) {...return into(
glideContext.buildImageViewTarget(view, transcodeClass),
/*targetListener=*/ null,
requestOptions,
Executors.mainThreadExecutor());
}
private <Y extends Target<TranscodeType>> Y into(
@NonNull Y target,
@NullableRequestListener<TranscodeType> targetListener, BaseRequestOptions<? > options, Executor callbackExecutor) {
Preconditions.checkNotNull(target);
if(! isModelSet) {throw new IllegalArgumentException("You must call #load() before calling #into()");
}
// Build a Request object that represents the loading taskRequest request = buildRequest(target, targetListener, options, callbackExecutor); ... requestManager. The clear (target); target.setRequest(request);// Start tracking the task by passing the request to the requestManager
requestManager.track(target, request);
return target;
}
Copy the code
The key is the requestManager.track(Target, Request) code, which is the initiation point of the task
public class RequestManager implements ComponentCallbacks2.LifecycleListener.ModelTypes<RequestBuilder<Drawable>> {
// Store all tasks
@GuardedBy("this")
private final RequestTracker requestTracker;
@GuardedBy("this")
private final TargetTracker targetTracker = new TargetTracker();
synchronized void track(@NonNullTarget<? > target,@NonNull Request request) {
targetTracker.track(target);
// Run the taskrequestTracker.runRequest(request); }}Copy the code
RequestTracker is used to store all requests, that is, all image-loading tasks, and provides methods to start, pause, and restart all tasks. The external controls whether the task is currently allowed to start by changing the value of the isPaused variable. The runRequest method uses isPaused to determine whether to start the task immediately or to store the task in pendingRequests
public class RequestTracker {
private static final String TAG = "RequestTracker";
private final Set<Request> requests =
Collections.newSetFromMap(new WeakHashMap<Request, Boolean>());
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private final List<Request> pendingRequests = new ArrayList<>();
private boolean isPaused;
/** Starts tracking the given request. */
public void runRequest(@NonNull Request request) {
// Save the task first
requests.add(request);
// Start the task if it is not paused, otherwise save it to the to-do list
if(! isPaused) { request.begin(); }else {
request.clear();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Paused, delaying request"); } pendingRequests.add(request); }}/** Stops any in progress requests. */
public void pauseRequests(a) {
isPaused = true;
for (Request request : Util.getSnapshot(requests)) {
if (request.isRunning()) {
// Avoid clearing parts of requests that may have completed (thumbnails) to avoid blinking
// in the UI, while still making sure that any in progress parts of requests are immediately
// stopped.request.pause(); pendingRequests.add(request); }}}/** Restarts failed requests and cancels and restarts in progress requests. */
public void restartRequests(a) {
for (Request request : Util.getSnapshot(requests)) {
if(! request.isComplete() && ! request.isCleared()) { request.clear();if(! isPaused) { request.begin(); }else {
// Ensure the request will be restarted in onResume.pendingRequests.add(request); }}}} ···}Copy the code
When SupportRequestManagerFragment in onStop (), can use RequestTracker, transfer will isPaused set to true. In addition, when SupportRequestManagerFragment execution to onDestroy (), means that the Activity has been finish, A callback notification is made to the RequestManager’s onDestroy() method, where tasks are cleaned up and various registration events are unregistered
@Override
public synchronized void onDestroy(a) {
targetTracker.onDestroy();
for(Target<? > target : targetTracker.getAll()) { clear(target); } targetTracker.clear(); requestTracker.clearRequests(); lifecycle.removeListener(this);
lifecycle.removeListener(connectivityMonitor);
mainHandler.removeCallbacks(addSelfToLifecycle);
glide.unregisterRequestManager(this);
}
Copy the code
Five, the specific process of loading pictures
Request is an interface that represents each image loading Request. It contains several implementation classes, such as SingleRequest. SingleRequest’s begin() method checks the current task status to prevent repeated loading, then gets the target width and height or ImageView width and height, and then determines whether the placeholders need to be shown first
public final class SingleRequest<R> implements Request.SizeReadyCallback.ResourceCallback {
@Override
public void begin(a) {
synchronized (requestLock) {
assertNotCallingCallbacks();
stateVerifier.throwIfRecycled();
startTime = LogTime.getLogTime();
if (model == null) {
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
width = overrideWidth;
height = overrideHeight;
}
// Only log at more verbose log levels if the user has set a fallback drawable, because
// fallback Drawables indicate the user expects null models occasionally.
int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
// If model is null, it indicates that no image source address is passed in
onLoadFailed(new GlideException("Received null model"), logLevel);
return;
}
// Prevent repeated startup while tasks are running
if (status == Status.RUNNING) {
throw new IllegalArgumentException("Cannot restart a running request");
}
if (status == Status.COMPLETE) {
// Return the loaded image resource
onResourceReady(resource, DataSource.MEMORY_CACHE);
return;
}
// Restarts for requests that are neither complete nor running can be treated as new requests
// and can run again from the beginning.
// First get the target width and height or ImageView width and load as needed
status = Status.WAITING_FOR_SIZE;
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
onSizeReady(overrideWidth, overrideHeight);
} else {
target.getSize(this);
}
if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
&& canNotifyStatusChanged()) {
// Pass out the placeholder first
target.onLoadStarted(getPlaceholderDrawable());
}
if (IS_VERBOSE_LOGGABLE) {
logV("finished run method in "+ LogTime.getElapsedMillis(startTime)); }}}}Copy the code
As you can see, the above logic does not cover the specific image loading logic, because this process needs to be done after the target width and height are obtained. If a specific width and height value is passed in from the outside, the external value will prevail, otherwise the target (such as ImageView) will prevail. Only after the width and height are obtained will the load actually start, all in order to achieve on-demand loading and avoid memory waste
So, the focus is on the onSizeReady method. All the configuration information (image address, width, height, priority, cache allowed, etc.) is transferred to the Engine’s load method to load the image
private volatile Engine engine;
/** A callback method that should never be invoked directly. */
@Override
public void onSizeReady(int width, int height) {
stateVerifier.throwIfRecycled();
synchronized (requestLock) {
if (IS_VERBOSE_LOGGABLE) {
logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
if(status ! = Status.WAITING_FOR_SIZE) {return;
}
status = Status.RUNNING;
// Scale
float sizeMultiplier = requestOptions.getSizeMultiplier();
this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
this.height = maybeApplySizeMultiplier(height, sizeMultiplier);
if (IS_VERBOSE_LOGGABLE) {
logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
}
// Start loading images
loadStatus =
engine.load(
glideContext,
model,
requestOptions.getSignature(),
this.width,
this.height,
requestOptions.getResourceClass(),
transcodeClass,
priority,
requestOptions.getDiskCacheStrategy(),
requestOptions.getTransformations(),
requestOptions.isTransformationRequired(),
requestOptions.isScaleOnlyOrNoTransform(),
requestOptions.getOptions(),
requestOptions.isMemoryCacheable(),
requestOptions.getUseUnlimitedSourceGeneratorsPool(),
requestOptions.getUseAnimationPool(),
requestOptions.getOnlyRetrieveFromCache(),
this,
callbackExecutor);
// This is a hack that's only useful for testing right now where loads complete synchronously
// even though under any executor running on any thread but the main thread, the load would
// have completed asynchronously.
if(status ! = Status.RUNNING) { loadStatus =null;
}
if (IS_VERBOSE_LOGGABLE) {
logV("finished onSizeReady in "+ LogTime.getElapsedMillis(startTime)); }}}Copy the code
The configuration information passed to the Engine also contains a ResourceCallback object, called SingleRequest itself, because SingleRequest implements the ResourceCallback interface. From the names of the methods included in ResourceCallback, you can see that the Engine uses these methods to call back the results when the image loading succeeded or failed
public interface ResourceCallback {
void onResourceReady(Resource
resource, DataSource dataSource);
void onLoadFailed(GlideException e);
Object getLock(a);
}
Copy the code
The load method generates a unique key for the request. The key is used to determine whether the image can be reused. Then, the value is obtained from the memory cache based on this key. Or add a callback to an existing task
public <R> LoadStatus load(
GlideContext glideContext,
Object model,
Key signature,
int width,
intheight, 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;
// Generate a unique identification key for this request. This key is the basis for determining whether the image reuse can be realizedEngineKey key = keyFactory.buildKey( model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource<? > memoryResource;synchronized (this) {
// Select value from memory cache first
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
if (memoryResource == null) {
// If the target resource does not exist in memory, start a new task to load it, or add a callback to an existing task
returnwaitForExistingOrStartNewJob( glideContext, model, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, options, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache, cb, callbackExecutor, key, startTime); }}// Avoid calling back while holding the engine lock, doing so makes it easier for callers to
// deadlock.
cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
return null;
}
Copy the code
For the request of the load network image, waitForExistingOrStartNewJob method for the image corresponds to the request through the network or is the process of loading a local disk file, if the target image has not been downloaded to the request for network, if has been to the local cache before to a disk loading. The loadFromMemory method corresponds to the process of trying to find the target image in memory, because the target image may have been loaded into memory before, and is used to try to overuse the image resource in memory
Here’s an example of loading a network image, from network request to disk cache to memory cache
1. Network request
Glide. With (Context).load(Any) is a multi-overloaded method that supports multiple input types such as Integer, String, Uri, File, etc. And finally we may get Bitmap, Drawable, GifDrawable and other results. So, how does Glide distinguish between our different entry requests? And how do you handle different request types?
Glide class contains a Registry variable, which acts as a registry that stores the processing logic for a particular input parameter type, and the resulting value type expected from that input parameter type
registry
.append(Uri.class, InputStream.class, new UriLoader.StreamFactory(contentResolver))
.append(
Uri.class,
ParcelFileDescriptor.class,
new UriLoader.FileDescriptorFactory(contentResolver))
.append(
Uri.class,
AssetFileDescriptor.class,
new UriLoader.AssetFileDescriptorFactory(contentResolver))
.append(Uri.class, InputStream.class, new UrlUriLoader.StreamFactory())
.append(URL.class, InputStream.class, new UrlLoader.StreamFactory())
.append(Uri.class, File.class, new MediaStoreFileLoader.Factory(context))
.append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())
.append(byte[].class, ByteBuffer.class, new ByteArrayLoader.ByteBufferFactory())
.append(byte[].class, InputStream.class, new ByteArrayLoader.StreamFactory())
.append(Uri.class, Uri.class, UnitModelLoader.Factory.<Uri>getInstance())
.append(Drawable.class, Drawable.class, UnitModelLoader.Factory.<Drawable>getInstance())
.append(Drawable.class, Drawable.class, new UnitDrawableDecoder())
/* Transcoders */
.register(Bitmap.class, BitmapDrawable.class, new BitmapDrawableTranscoder(resources))
.register(Bitmap.class, byte[].class, bitmapBytesTranscoder)
.register(
Drawable.class,
byte[].class,
new DrawableBytesTranscoder(
bitmapPool, bitmapBytesTranscoder, gifDrawableBytesTranscoder))
.register(GifDrawable.class, byte[].class, gifDrawableBytesTranscoder);
Copy the code
For example, one of the most common ways we request images from the network is through the image Url, which corresponds to the following configuration. The GlideUrl corresponds to the ImageUrl we passed in, and the InputStream is the resource InputStream that we want to obtain from the network according to the Url. The HttpGlideUrlLoader is used to implement the process of converting the ImageUrl to the InputStream
append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())
Copy the code
The HttpGlideUrlLoader will pass the ImageUrl to the HttpUrlFetcher to make the specific network request
public class HttpGlideUrlLoader implements ModelLoader<GlideUrl.InputStream> {
@Override
public LoadData<InputStream> buildLoadData(
@NonNull GlideUrl model, int width, int height, @NonNull Options options) {
// GlideUrls memoize parsed URLs so caching them saves a few object instantiations and time
// spent parsing urls.
GlideUrl url = model;
if(modelCache ! =null) {
url = modelCache.get(model, 0.0);
if (url == null) {
modelCache.put(model, 0.0, model); url = model; }}int timeout = options.get(TIMEOUT);
return new LoadData<>(url, newHttpUrlFetcher(url, timeout)); }}Copy the code
HttpUrlFetcher in loadDataWithRedirects method through HttpURLConnection to request the image, and finally through DataCallback to get the image InputStream InputStream object out through. In addition, the loadDataWithRedirects method directs itself by calling the loadDataWithRedirects loop. You are not allowed to repeat redirects to the same Url up to five times, otherwise you go through the failed process
public class HttpUrlFetcher implements DataFetcher<InputStream> {
private static final int MAXIMUM_REDIRECTS = 5;
@Override
public void loadData(
@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
long startTime = LogTime.getLogTime();
try {
InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0.null, glideUrl.getHeaders());
callback.onDataReady(result);
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to load data for url", e);
}
callback.onLoadFailed(e);
} finally {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Finished http url fetcher fetch in "+ LogTime.getElapsedMillis(startTime)); }}}private InputStream loadDataWithRedirects(
URL url, int redirects, URL lastUrl, Map<String, String> headers) throws IOException {
if (redirects >= MAXIMUM_REDIRECTS) {
// When the total number of redirects reaches five, the process fails
throw new HttpException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");
} else {
// Comparing the URLs using .equals performs additional network I/O and is generally broken.
// See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
try {
if(lastUrl ! =null && url.toURI().equals(lastUrl.toURI())) {
// Loop redirection to the same Url, go through the failure process
throw new HttpException("In re-direct loop"); }}catch (URISyntaxException e) {
// Do nothing, this is best effort.} } urlConnection = connectionFactory.build(url); ... the stream = urlConnection. GetInputStream ();if (isCancelled) {
return null;
}
final int statusCode = urlConnection.getResponseCode();
if (isHttpOk(statusCode)) {
return getStreamForSuccessfulRequest(urlConnection);
} else if (isHttpRedirect(statusCode)) {
String redirectUrlString = urlConnection.getHeaderField("Location");
if (TextUtils.isEmpty(redirectUrlString)) {
throw new HttpException("Received empty or null redirect url");
}
URL redirectUrl = new URL(url, redirectUrlString);
// Closing the stream specifically is required to avoid leaking ResponseBodys in addition
// to disconnecting the url connection below. See #2352.
cleanup();
return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
} else if (statusCode == INVALID_STATUS_CODE) {
throw new HttpException(statusCode);
} else {
throw newHttpException(urlConnection.getResponseMessage(), statusCode); }}}Copy the code
2. Disk cache
Look back Engine class waitForExistingOrStartNewJob method. When it is determined that there is no target image in the current memory cache, EngineJob and DecodeJob will be started for disk file loading or network request loading
private <R> LoadStatus waitForExistingOrStartNewJob(
GlideContext glideContext,
Object model,
Key signature,
int width,
intheight, 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,
EngineKey key,
long startTime) { EngineJob<? > current = jobs.get(key, onlyRetrieveFromCache);if(current ! =null) {
// If the same request task has already been started, add a callback to it
current.addCallback(cb, callbackExecutor);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Added to existing load", startTime, key);
}
return new LoadStatus(cb, current);
}
EngineJob<R> engineJob =
engineJobFactory.build(
key,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache);
DecodeJob<R> decodeJob =
decodeJobFactory.build(
glideContext,
model,
key,
signature,
width,
height,
resourceClass,
transcodeClass,
priority,
diskCacheStrategy,
transformations,
isTransformationRequired,
isScaleOnlyOrNoTransform,
onlyRetrieveFromCache,
options,
engineJob);
jobs.put(key, engineJob);
engineJob.addCallback(cb, callbackExecutor);
/ / start decodeJob
engineJob.start(decodeJob);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}
Copy the code
We’ll focus on the DecodeJob class here. As mentioned earlier, Glide can be divided into two types of images that are cached on disk, one is the original image, the other is the original image after various conversion operations, such as compression, cropping, transformation, etc. This behavior is determined by the diskCacheStrategy parameter
Glide.with(context).load(imageUrl)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.into(imageView)
Copy the code
If we are using DiskCacheStrategy.data, we will cache the original image and try to load the locally cached original image at the time of loading. This property affects both write and read operations. The DecodeJob will select the corresponding DataFetcherGenerator for processing according to our cache configuration, so there are three possible source types for the final image:
- Reuse converted picture resources. Corresponding to ResourceCacheGenerator, if the cache does not match, go to the next step
- Reuse the original image resources. Corresponding to DataCacheGenerator, the next step is performed when the cache does not hit
- No local cached resource meets the requirements, and a new load is required (networking request). Corresponding to the SourceGenerator
private DataFetcherGenerator getNextGenerator(a) {
switch (stage) {
case RESOURCE_CACHE:
return new ResourceCacheGenerator(decodeHelper, this);
case DATA_CACHE:
return new DataCacheGenerator(decodeHelper, this);
case SOURCE:
return new SourceGenerator(decodeHelper, this);
case FINISHED:
return null;
default:
throw new IllegalStateException("Unrecognized stage: "+ stage); }}Copy the code
For example, the main logic of DataCacheGenerator is the startNext() method, which takes the original image from DiskCache and gets the cacheFile cacheFile and the corresponding processor modelLoaders, ModelLoaders contains all implementers that can perform the conversion operation (e.g., File to Drawable, File to Bitmap, etc.), and returns true if the cache File and the corresponding converter are determined. When the DataCacheGenerator succeeds in loading the Target data, it calls back to the DecodeJob’s onDataFetcherReady method, eventually storing the Target data in ActiveResources and notifying all targets
@Override
public boolean startNext(a) {
while (modelLoaders == null| |! hasNextModelLoader()) { sourceIdIndex++;if (sourceIdIndex >= cacheKeys.size()) {
return false;
}
Key sourceId = cacheKeys.get(sourceIdIndex);
// PMD.AvoidInstantiatingObjectsInLoops The loop iterates a limited number of times
// and the actions it performs are much more expensive than a single allocation.
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
// Take the value from the disk cache
cacheFile = helper.getDiskCache().get(originalKey);
if(cacheFile ! =null) {
this.sourceKey = sourceId;
// Get all the data type converters
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())) {
started = true;
loadData.fetcher.loadData(helper.getPriority(), this); }}return started;
}
Copy the code
The DataCacheGenerator represents the case where the target image is fetched from the local disk cache, and the logic for requesting the network image and writing it to the local disk depends on the SourceGenerator
The SourceGenerator calls the onDataReadyInternal method after the image has been successfully loaded via HttpUrlFetcher. If disk caching is not allowed on this request, the DecodeJob’s onDataFetcherReady method is called directly to complete the process, consistent with the DataCacheGenerator. If disk caching is allowed, the startNext() method is called to reschedule(), the disk file is written to the cacheData method, and a DataCacheGenerator is constructed. The value is then taken from the disk by DataCacheGenerator
void onDataReadyInternal(LoadData
loadData, Object data) {
DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
if(data ! =null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
// To allow disk caching, cache data in the dataToCache variable
dataToCache = data;
// We might be being called back on someone else's thread. Before doing anything, we should
// reschedule to get back onto Glide's thread.
cb.reschedule();
} else{ cb.onDataFetcherReady( loadData.sourceKey, data, loadData.fetcher, loadData.fetcher.getDataSource(), originalKey); }}@Override
public boolean startNext(a) {
if(dataToCache ! =null) {
//dataToCache is not null, indicating that it is time to cache images to disk
Object data = dataToCache;
dataToCache = null;
cacheData(data);
}
if(sourceCacheGenerator ! =null && sourceCacheGenerator.startNext()) {
return true; }...return started;
}
private void cacheData(Object dataToCache) {
long startTime = LogTime.getLogTime();
try {
Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
DataCacheWriter<Object> writer =
new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
// Write to the disk cache
helper.getDiskCache().put(originalKey, writer);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(
TAG,
"Finished encoding source to cache"
+ ", key: "
+ originalKey
+ ", data: "
+ dataToCache
+ ", encoder: "
+ encoder
+ ", duration: "+ LogTime.getElapsedMillis(startTime)); }}finally {
loadData.fetcher.cleanup();
}
sourceCacheGenerator =
new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
}
Copy the code
Glide’s disk caching algorithm specifically corresponds to the DiskLruCache class, which is Glide based on JakeWharton’s DiskLruCache open source library modified, here is not too much detail
No matter how the DecodeJob gets the images, it will eventually call the onEngineJobComplete method of the Engine class, which will cache the loaded images into memory. This is the source of the in-memory cache data
@Override
public synchronized void onEngineJobComplete( EngineJob
engineJob, Key key, EngineResource
resource) {
// A null resource indicates that the load failed, usually due to an exception.
if(resource ! =null && resource.isMemoryCacheable()) {
activeResources.activate(key, resource);
}
jobs.removeIfCurrent(key, engineJob);
}
Copy the code
3. Memory cache
Let’s look at the memory caching mechanism. Glide’s MemoryCache is divided into two levels: ActiveResources and MemoryCache. The operation of MemoryCache corresponds to the loadFromMemory method of Engine class
- Value from ActiveResources based on key, and call if obtained
acquire()
Method to increase the number of references to the resource by one, otherwise go to the next step - Calls based on the value of the key from MemoryCache, if it is obtained
acquire()
Method increments the number of references to the resource by one and simultaneously removes the resource from MemoryCache and stores it in ActiveResources, returning null if no value is obtained
private final ActiveResources activeResources;
private final MemoryCache cache;
// Try to load the image resource from memory
@Nullable
privateEngineResource<? > loadFromMemory( EngineKey key,boolean isMemoryCacheable, long startTime) {
if(! isMemoryCacheable) {// If the memory cache is not allowed, return it directly
return null;
}
// Load from ActiveResourcesEngineResource<? > active = loadFromActiveResources(key);if(active ! =null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
// Load from MemoryCacheEngineResource<? > cached = loadFromCache(key);if(cached ! =null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
return null;
}
@Nullable
privateEngineResource<? > loadFromActiveResources(Key key) { EngineResource<? > active = activeResources.get(key);if(active ! =null) {
active.acquire();
}
return active;
}
privateEngineResource<? > loadFromCache(Key key) { EngineResource<? > cached = getEngineResourceFromCache(key);if(cached ! =null) {
cached.acquire();
activeResources.activate(key, cached);
}
return cached;
}
privateEngineResource<? > getEngineResourceFromCache(Key key) { Resource<? > cached = cache.remove(key);finalEngineResource<? > result;if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
// Save an object allocation if we've cached an EngineResource (the typical case).result = (EngineResource<? >) cached; }else {
result =
new EngineResource<>(
cached, /*isMemoryCacheable=*/ true./*isRecyclable=*/ true, key, /*listener=*/ this);
}
return result;
}
Copy the code
ActiveResources is a weak reference to all currently in use image resources. As we know, if an object has only weak references and is no longer strongly referenced, then when GC occurs, the references held in the weak reference are directly nulled and the weak reference object itself is stored in the associated ReferenceQueue
When a new image is loaded and used, and memory caching is currently allowed, the image resource is saved to the activeEngineResources via the activate method. When the reference count of an image resource becomes 0, it indicates that the resource is no longer in external use. In this case, the deactivate method will be used to remove it from the activeEngineResources to eliminate the reference to the resource. The resource is also stored in MemoryCache if memory caching is currently allowed
final class ActiveResources {
final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
private finalReferenceQueue<EngineResource<? >> resourceReferenceQueue =new ReferenceQueue<>();
synchronized void activate(Key key, EngineResource
resource) {
ResourceWeakReference toPut =
new ResourceWeakReference(
key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
ResourceWeakReference removed = activeEngineResources.put(key, toPut);
if(removed ! =null) { removed.reset(); }}synchronized void deactivate(Key key) {
ResourceWeakReference removed = activeEngineResources.remove(key);
if(removed ! =null) { removed.reset(); }}@Synthetic
void cleanupActiveReference(@NonNull ResourceWeakReference ref) {
synchronized (this) {
activeEngineResources.remove(ref.key);
if(! ref.isCacheable || ref.resource ==null) {
return; } } EngineResource<? > newResource =new EngineResource<>(
ref.resource, /*isMemoryCacheable=*/ true./*isRecyclable=*/ false, ref.key, listener); listener.onResourceReleased(ref.key, newResource); }}// Corresponds to the Engine class
@Override
public void onResourceReleased(Key cacheKey, EngineResource
resource) {
// Remove the image resource from activeResources
activeResources.deactivate(cacheKey);
if (resource.isMemoryCacheable()) {
// If memory caching is allowed, the image resources are stored in MemoryCache
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource, /*forceNextFrame=*/ false); }}Copy the code
The default implementation of MemoryCache corresponds to the LruResourceCache class. As the name suggests, MemoryCache uses an Lru algorithm, which caches images based on the maximum MemoryCache size passed in from outside. The logic itself is relatively simple, but I don’t need to go into much detail
LruResourceCache contains a ResourceRemovedListener object that is used to notify the Engine when an image object has been removed from the memory cache. The Engine recycles the image resource
public class LruResourceCache extends LruCache<Key.Resource<? >>implements MemoryCache {
@Override
public void setResourceRemovedListener(@NonNull ResourceRemovedListener listener) {
this.listener = listener;
}
@Override
protected void onItemEvicted(@NonNull Key key, @NullableResource<? > item) {
if(listener ! =null&& item ! =null) { listener.onResourceRemoved(item); }}}Copy the code
So let’s summarize the logic and relationship between ActiveResources and MemoryCache
- ActiveResources holds a weak reference to an image resource that is currently in use. When an image is loaded successfully and still in use, ActiveResources holds a reference to it. When images are no longer used, they are removed from ActiveResources and stored in MemoryCache
- MemoryCache uses the Lrc algorithm to cache image resources in memory, only those that are not currently in use. When an image cached in the MemoryCache is reused externally, the image is removed from the MemoryCache and restored in ActiveResources
- In ActiveResources, the image stored in ActiveResources is a resource that is currently in a strong reference state. So Glide’s memory cache value from ActiveResources will not increase the current used memory. As the system memory size is limited, MemoryCache uses THE Lrc algorithm to save memory as much as possible and to maximize the probability that the images will be reused can be retained
- Glide divides the MemoryCache into ActiveResources and MemoryCache, rather than putting them all into MemoryCache, thus avoiding the error of removing currently active image resources from the queue. In addition, ActiveResources also circularly determines whether the saved image resources are no longer used externally, so that MemoryCache can be updated in a timely manner and the utilization and accuracy of MemoryCache can be improved
Vi. Memory cleaning mechanism
Glide’s memoryCache mechanism is to reuse image resources as much as possible to avoid frequent disk read and write and memory read and write. MemoryCache, bitmapPool and arrayPool exist for this purpose, but on the other hand, memoryCache also causes a part of memory space to be occupied all the time. The available memory space of the system may be insufficient. When our application is back to the background, if the available memory space of the system is insufficient, then the system will clean up some background processes according to the priority, in order to free up memory space for the foreground process, in order to improve the priority of the application in the background, we need to take the initiative to reduce our memory usage
Fortunately, Glide also considers this situation and provides an automatic cleaning mechanism for cache memory. Glide’s initializeGlide method registers a ComponentCallbacks to Application by default to receive notifications of memory state changes sent by the system
@GuardedBy("Glide.class")
@SuppressWarnings("deprecation")
private static void initializeGlide(
@NonNull Context context,
@NonNull GlideBuilder builder,
@Nullable GeneratedAppGlideModule annotationGeneratedModule) { Context applicationContext = context.getApplicationContext(); ... applicationContext. RegisterComponentCallbacks (glide); Glide.glide = glide; }Copy the code
The corresponding ComponentCallbacks implementation class is Glide class itself, and its related method implementation corresponds to the following two
@Override
public void onTrimMemory(int level) {
trimMemory(level);
}
@Override
public void onLowMemory(a) {
clearMemory();
}
Copy the code
These two methods automatically trigger the cleanup of memoryCache, bitmapPool, and arrayPool
public void trimMemory(int level) {
// Engine asserts this anyway when removing resources, fail faster and consistently
Util.assertMainThread();
// Request managers need to be trimmed before the caches and pools, in order for the latter to
// have the most benefit.
for (RequestManager manager : managers) {
manager.onTrimMemory(level);
}
// memory cache needs to be trimmed before bitmap pool to trim re-pooled Bitmaps too. See #687.
memoryCache.trimMemory(level);
bitmapPool.trimMemory(level);
arrayPool.trimMemory(level);
}
public void clearMemory(a) {
// Engine asserts this anyway when removing resources, fail faster and consistently
Util.assertMainThread();
// memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687.
memoryCache.clearMemory();
bitmapPool.clearMemory();
arrayPool.clearMemory();
}
Copy the code
Include several thread pools
To conclude, Glide is a total of seven thread pools, if I’m not missing anything. Here I am referring to the thread pool not only refers to the concept of ThreadPoolExecutor class, it is the Java. Util. Concurrent. Any implementation of the Executor interface classes
Of these, the first four thread pools can be answered by the constructor parameter of the EngineJob class
class EngineJob<R> implements DecodeJob.Callback<R>, Poolable { EngineJob( GlideExecutor diskCacheExecutor, GlideExecutor sourceExecutor, GlideExecutor sourceUnlimitedExecutor, GlideExecutor animationExecutor, EngineJobListener engineJobListener, ResourceListener resourceListener, Pools.Pool<EngineJob<? >> pool) {this( diskCacheExecutor, sourceExecutor, sourceUnlimitedExecutor, animationExecutor, engineJobListener, resourceListener, pool, DEFAULT_FACTORY); }}Copy the code
Its uses are:
- DiskCacheExecutor. Used to load the disk cache
- SourceExecutor. Used to perform operations that do not load the local disk cache, such as loading images based on the specified URI or ImageUrl
- SourceUnlimitedExecutor. With sourceExecutor
- AnimationExecutor. The official annotation is used to load giFs
The four thread pools are created using the GlideExecutor class.
- DiskCacheExecutor. The number of core threads and the maximum number of threads are both 1, and the thread timeout is 0 seconds. Because diskCacheExecutor reads and writes disk files, both the core thread count and the maximum thread count are one, so that only one thread is active at all times when the thread pool is started, ensuring file read and write order and avoiding locking operations
- SourceExecutor. The number of core threads and the maximum number of threads are determined by the number of cpus on the device, which is at least 4 threads with a thread timeout of 0 seconds. The thread count setting limits Glide to a maximum of four network load image requests
- SourceUnlimitedExecutor. The number of core threads is 0, the maximum number of threads is integer.max_value, and the timeout time is 10 seconds. When the thread is idle, it will be reclaimed immediately. The purpose of sourceUnlimitedExecutor is to cope with the need to process a large number of load image requests simultaneously, allowing nearly unlimited number of new threads to handle each request, which may improve the timeliness of sourceExecutor. However, it can also reduce efficiency due to multi-threaded competition, and it is also easy to OOM
- AnimationExecutor. If the number of cpus on the device is greater than 4, the number of core threads and the maximum number of threads are set to 2; otherwise, the number is set to 1. The thread timeout is 0 seconds
All four thread pools are used for the EngineJob class. DiskCacheExecutor is only used for disk caching and will be used as long as disk caching is allowed for this request. And three other thread pool in my point of view is used to load a local file or a network request pictures, if useUnlimitedSourceGeneratorPool is true, just use sourceUnlimitedExecutor, Otherwise, if useAnimationPool is true, animationExecutor is used, otherwise sourceExecutor is used
UseUnlimitedSourceGeneratorPool well understand the meaning of, that is, in order to control the maximum number of threads concurrent requests at the same time, but the meaning of distinguish useAnimationPool I don’t understand, understand the classmates to solve the trouble
public synchronized void start(DecodeJob<R> decodeJob) {
this.decodeJob = decodeJob;
GlideExecutor executor =
decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();
executor.execute(decodeJob);
}
private GlideExecutor getActiveSourceExecutor(a) {
return useUnlimitedSourceGeneratorPool
? sourceUnlimitedExecutor
: (useAnimationPool ? animationExecutor : sourceExecutor);
}
Copy the code
The fifth thread pool is located in the ActiveResources class. This thread pool is used to continuously determine values in the ReferenceQueue to cache image resources that are currently no longer in external use into MemoryCache
ActiveResources(boolean isActiveResourceRetentionAllowed) {
this(
isActiveResourceRetentionAllowed,
java.util.concurrent.Executors.newSingleThreadExecutor(
new ThreadFactory() {
@Override
public Thread newThread(@NonNull final Runnable r) {
return new Thread(
new Runnable() {
@Override
public void run(a) { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); r.run(); }},"glide-active-resources"); }})); }Copy the code
The remaining two thread pools are in the Executors class
-
MAIN_THREAD_EXECUTOR. Use Handler to switch to the main thread to update the UI when the image has been loaded
-
DIRECT_EXECUTOR. This is an empty implementation that executes Runnable on the original thread, and is used when we want to get images directly instead of updating the UI, such as Glide. With (this).load(URL).submit()
public final class Executors {
private Executors(a) {
// Utility class.
}
private static final Executor MAIN_THREAD_EXECUTOR =
new Executor() {
private final Handler handler = new Handler(Looper.getMainLooper());
@Override
public void execute(@NonNull Runnable command) { handler.post(command); }};private static final Executor DIRECT_EXECUTOR =
new Executor() {
@Override
public void execute(@NonNull Runnable command) { command.run(); }};/** Posts executions to the main thread. */
public static Executor mainThreadExecutor(a) {
return MAIN_THREAD_EXECUTOR;
}
/** Immediately calls {@link Runnable#run()} on the current thread. */
public static Executor directExecutor(a) {
return DIRECT_EXECUTOR;
}
@VisibleForTesting
public static void shutdownAndAwaitTermination(ExecutorService pool) {
long shutdownSeconds = 5;
pool.shutdownNow();
try {
if(! pool.awaitTermination(shutdownSeconds, TimeUnit.SECONDS)) { pool.shutdownNow();if(! pool.awaitTermination(shutdownSeconds, TimeUnit.SECONDS)) {throw new RuntimeException("Failed to shutdown"); }}}catch (InterruptedException ie) {
pool.shutdownNow();
Thread.currentThread().interrupt();
throw newRuntimeException(ie); }}}Copy the code
Custom network request library
By default, Glide loads images over the network using HttpURLConnection, which is relatively raw and inefficient compared to our usual OkHttp. Glide also provides a Registry class that allows external definitions to implement specific request logic
For example, if you want to request images via OkHttp, you can rely on Glide’s official support library:
dependencies {
implementation "Com. Making. Bumptech. Glide: okhttp3 - integration: 4.11.0"
}
Copy the code
Glide automatically assigns network type requests to its internal OkHttp as long as okHttp3-integration is in place. Because it contains an OkHttpLibraryGlideModule class that declares the @glidemodule annotation, it can be resolved by Glide at run time, The GlideUrl type load request is then passed to OkHttpUrlLoader for processing
@GlideModule
public final class OkHttpLibraryGlideModule extends LibraryGlideModule {
@Override
public void registerComponents(
@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.replace(GlideUrl.class, InputStream.class, newOkHttpUrlLoader.Factory()); }}Copy the code
We can also copy the code from okHttp3-Integration and pass in our own OkHttpUrlLoader in our custom AppGlideModule class
@GlideModule
class MyAppGlideModule : AppGlideModule() {
override fun isManifestParsingEnabled(a): Boolean {
return false
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val okHttClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.eventListener(object : EventListener() {
override fun callStart(call: okhttp3.Call) {
Log.e("TAG"."CallStart." + call.request().url().toString())
}
}).build()
registry.replace(
GlideUrl::class.java, InputStream::class.java,
OkHttpUrlLoader.Factory(okHttClient)
)
}
}
Copy the code