Let’s first look at two aspects of picture loading performance optimization in Glide official documentation:
- Picture decoding speed
- Resource pressure from decoding images
The main steps are as follows:
- Automatic and intelligent downsampling and caching to minimize storage overhead and decoding times;
- Aggressive resource reuse, such as byte arrays and bitmaps, to minimize the impact of expensive garbage collection and heap fragmentation;
- Deep lifecycle integration to ensure that only active Fragment and Activity requests are processed first, and to help applications free resources if necessary to avoid being killed in the background.
Glide cache mechanism
Official document has detailed instructions muyangmin. Making. IO/glide – docs -…
Multi-level cache logic
Multistage cache
- Active Resources – Is there another View showing this image right now?
- Memory cache – Has the image been loaded recently and still exists in Memory?
- Resource Type – Has this image been decoded, converted, and written to disk cache before?
- Data Source – Was the resource used to build this image previously written to the file cache?
The first two steps check if the image is in memory, and if so, return the image directly. The last two steps check that the picture is on disk so that it can be returned quickly but asynchronously. If all four steps fail to find the image, Glide returns to the original resource to retrieve the data (original file, Uri, Url, etc.).
Caching strategies
Memory caching strategy
Active and in-memory resources are cached in memory. Active resources are stored by weak reference of HashMap. Memory cache resources are stored by LRU cache.
Disk cache policy types (see DiskCacheStrategy) :
- Diskcachestrategy. DATA: DATA written to a disk is the original DATA that has not been modified by the loading process.
- DiskCacheStrategy. RESOURCE: the decoding, after the transformation of RESOURCE to disk;
- Diskcachestrategy. ALL: The remote resource (URL resource) writes both the original resource and the transformed resource. Local file resources will only be written to the resources after decoding transformation;
- Diskcachestrategy. NONE: No data is written to the disk.
- DiskCacheStrategy. AUTOMATIC: it will try to use the best strategy for local and remote images. When you load remote data (for example, downloaded from a URL),
AUTOMATIC
Policies only store raw data that has not been modified (e.g. transformed) by your loading process, because downloading remote data is much more expensive than adjusting data that already exists on disk. For local data,AUTOMATIC
The strategy is to store only the thumbnails that have been transformed, because it’s easy to retrieve the original data even if you need to generate another image of a different size or type.
The cache keys
Glide builds different keys for different cache scenarios, and active resource and memory caches use slightly different keys than disk resource caches to accommodate memory, options, such as options that affect Bitmap configurations, or other parameters that are only used when decoding. Take the memory key value as an example. If the image size of the requested resource changes, the image cannot match the key in the cache. You need to remove the disk or obtain the image again from the network to change the image. Let’s look at the construction of the relevant Key;
EngineKey(
Object model,
Key signature,
int width,
intheight, Map<Class<? >, Transformation<? >> transformations, Class<? > resourceClass, Class<? > transcodeClass, Options options) ResourceCacheKey( ArrayPool arrayPool, Key sourceKey, Key signature,int width,
intheight, Transformation<? > appliedTransformation, Class<? > decodedResourceClass, Options Options DataCacheKey(Key sourceKey, Key signature)Copy the code
In Glide V4, all cache keys contain at least two elements:
- Request model (File, Url, Url) to load. If you use a custom Model, it needs to be implemented correctly
hashCode()
和equals()
- An optional one
The signature
(Signature)
In addition, the cache keys for steps 1-3(Active Resource, memory cache, Resource disk cache) also contain some additional data, including:
- Width and height
- optional
Transformation
- Any additional additions
Option (Options)
- The data type requested (Bitmap, GIF, or other)
Modify the default cache configuration
Glide also provides a way to modify the default cache configuration items;
// Change the size of the memory cache configuration (of course, you can customize your cache)
@GlideModule
public class YourAppGlideModule extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
int memoryCacheSizeBytes = 1024 * 1024 * 20; // 20mb
builder.setMemoryCache(newLruResourceCache(memoryCacheSizeBytes)); }}Copy the code
And configure the cache policy using the loading of an image:
Glide.with(fragment)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(imageView);
Copy the code
Glide cache source code analysis
The normal operation for loading an image is as follows:
RequestBuilder
Into (imageView) (scaleType = imageView) (scaleType = imageView) (scaleType = imageView) (scaleType = imageView) (scaleType = imageView) (scaleType = imageView) (scaleType = imageView)
Then enter:
into(
glideContext.buildImageViewTarget(view, transcodeClass),
/*targetListener=*/ null,
requestOptions,
Executors.mainThreadExecutor())
Copy the code
Then create a request via buildRequest and proceed
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()");
}
//1. Create a request
Request request = buildRequest(target, targetListener, options, callbackExecutor);
Request previous = target.getRequest();
if(request.isEquivalentTo(previous) && ! isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {// If two requests are the same, if the previous request fails, the request will be repeated
// If the last request was already running, it will continue to run without interrupting it.
if(! Preconditions.checkNotNull(previous).isRunning()) {// If the same request did not run, run the previous request directly.
// This can optimize some operations such as setting up station maintenance, recording, obtaining image information and so on in a request
previous.begin();
}
return target;
}
requestManager.clear(target);
target.setRequest(request);
// Start executing the request
requestManager.track(target, request);
return target;
}
Copy the code
SingleRequest
Finally through requestManager. Track (target, request) call RequestTracker. RunRequest, through SingleRequest. The begin, SingleRequest. OnSizeReady method into the Engine. The load method;
Let’s focus on the engine.load method
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;
// First, generate the raw cache keyEngineKey key = keyFactory.buildKey( model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource<? > memoryResource;synchronized (this) {
// Step 2, read the resource from memory
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
if (memoryResource == null) {
// Step 3, start a new task: read the required resources from disk or network.
return waitForExistingOrStartNewJob(
glideContext,
model,
signature,
width,
height,
resourceClass,
transcodeClass,
priority,
diskCacheStrategy,
transformations,
isTransformationRequired,
isScaleOnlyOrNoTransform,
options,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache,
cb,
callbackExecutor,
key,
startTime);
}
}
cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
return null;
}
Copy the code
Step 1: Generate the cached key, where a key is created based on the model, signature, width, transform, and original resource, transformed resource, and options described above. This is the original key, and subsequent execution to disk will generate a new key based on the original key. Step 2: Try to fetch the resource file from memory, which is divided into two parts: active resource and memory cache resource.
@Nullable
privateEngineResource<? > loadFromMemory( EngineKey key,boolean isMemoryCacheable, long startTime) {
if(! isMemoryCacheable) {return null;
}
// Activity resourcesEngineResource<? > active = loadFromActiveResources(key);if(active ! =null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
// Memory cache resourcesEngineResource<? > cached = loadFromCache(key);if(cached ! =null) {
return cached;
}
return null;
}
Copy the code
Activity resources The ultimate implementation of activity resources is through Activity Resources. Internally maintains a Map
, that is, a weakly referenced hashMap, which stores the active resource in the weak reference of the hashMap.
@VisibleForTesting final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
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(); }}Copy the code
Memory cache
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
The key implementation is the cache defined by MemoryCache. The implementation class of MemoryCache is LruResourceCache. LruResourceCache integrates LruCache, which implements LRU algorithm to maintain memory cache data.
Step 3: Start a new task: read the required resources from disk or network. Through waitForExistingOrStartNewJob into engineJob. Start (decodeJob); DecodeJob runs -> runWrapped -> runGenerators. DecodeJob runs -> runWrapped -> runGenerators
private void runGenerators(a) {
currentThread = Thread.currentThread();
startFetchTime = LogTime.getLogTime();
boolean isStarted = false;
while(! isCancelled && currentGenerator ! =null
&& !(isStarted = currentGenerator.startNext())) {
stage = getNextStage(stage);
currentGenerator = getNextGenerator();
if (stage == Stage.SOURCE) {
reschedule();
return; }}// We've run out of stages and generators, give up.
if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
notifyFailed();
}
}
Copy the code
Including currentGenerator. StartNext () method to actually perform loading, we’ll look at currentGenerator what are the specific implementation class.
If the current stage is returned according to the cache policy, the generator will be generated according to the current stage.
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:
// Skip loading from source if the user opted to only retrieve the resource from cache.
return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
case SOURCE:
case FINISHED:
return Stage.FINISHED;
default:
throw new IllegalArgumentException("Unrecognized stage: "+ current); }}Copy the code
The three producers are created based on the current phase
- The resource cache file corresponds to ResourceCacheGenerator
- No transformed metadata cache file corresponds to the DataCacheGenerator
- Source data SourceGenerator corresponding to data (network or local file)
The logic is to search the local resource cache file for the required resource, if not, search the metadata cache, if not, go to the source data to load, and write the current data to the disk cache.
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
ResourceCacheGenerator
public boolean startNext(a) {··· omit code currentKey =new ResourceCacheKey( // NOPMD AvoidInstantiatingObjectsInLoops
helper.getArrayPool(),
sourceId,
helper.getSignature(),
helper.getWidth(),
helper.getHeight(),
transformation,
resourceClass,
helper.getOptions());
// Determine whether the disk cache exists according to DiskLruCache
cacheFile = helper.getDiskCache().get(currentKey);
if(cacheFile ! =null) {
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())) {
started = true;
loadData.fetcher.loadData(helper.getPriority(), this); }}return started;
}
Copy the code
The default implementation is DiskLruCache, which reads the resource from DiskCache. After the resource is encoded, it is saved to disk.
DataCacheGenerator
DataCacheGenerator. StartNext logic and ResourceCacheGenerator basic same. First, create a DataCacheKey, which differs from the ResourceCacheGenerator Key generation method. The second step is to try to get the metadata cache from the third party through DiskCache and load the data according to modelLoader
SourceGenerator
Start by trying to load the source data, mainly through the DataFetcher, which selects the appropriate one to call based on the data source type.
private void startNextLoad(finalLoadData<? > toStart) {
loadData.fetcher.loadData(
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); }}}); }Copy the code
The DataFetcher implementation class is shown below.
Let’s go to HttpUrlFetcher and see how this works. Here we see the loadData withRedirects method inside the loadData method to get into the actual file network data reading.
private InputStream loadDataWithRedirects(
URL url, int redirects, URL lastUrl, Map<String, String> headers) throws IOException {
urlConnection = connectionFactory.build(url);
for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
}
urlConnection.setConnectTimeout(timeout);
urlConnection.setReadTimeout(timeout);
urlConnection.setUseCaches(false);
urlConnection.setDoInput(true);
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
OnDataFetcherReady calls DecodeJob and loadPath. load. Finally, the ResourceTranscoder is converted into BitmapResource, BitmapDrawableResource, FileResource and so on.