The Glide (trunk) of the Glide loading picture is introduced, and the details will be further implemented. This article is a bit larger, so you can follow the table of contents index to the chapters you are interested in

Source code based on the latest version of 4.11.0, the first responsibility map preview, the family will be neat ~


This article contains about 3200 words and takes about 10 minutes to read. If individual big picture is blurred (official will compress), can go to personal site to read

Generated API

Interface extensions are implemented by creating classes, inheriting the associated interfaces, and then annotating them to be handled by APT.

Global configuration

The @glidemodule annotation is used to configure global parameters and register custom capabilities. AppGlideModule for Application and LibraryGlideModule for Library.

@GlideModule
public class MyAppGlideModule extends AppGlideModule {
    @Override
    public boolean isManifestParsingEnabled(a) {
        return false;// The new version does not need to parse the metadata in the manifest.
 }   @Override  public void applyOptions(Context context, GlideBuilder builder) {  super.applyOptions(context, builder);  // Global configuration  //builder.setBitmapPool(xxx);  //builder.setDefaultRequestOptions(xxx);  / /...  }   @Override  public void registerComponents(Context context, Glide glide, Registry registry) {  super.registerComponents(context, glide, registry);  // Register some custom capabilities, such as extending the new image source ModelLoader  //registry.register(xxx);  } } Copy the code

For example, the default Bitmap configuration for Glide is ARGB_8888. If the project image type is relatively simple and does not require transparency channel and high color gambit, you can configure global RGB_565 to reduce memory by half. See default request options,

@GlideModule
public class MyAppGlideModule extends AppGlideModule {

    @Override
    public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
 super.applyOptions(context, builder);  builder.setDefaultRequestOptions(new RequestOptions()  .format(DecodeFormat.PREFER_RGB_565));  // Note: since PNG requires transparency channels, 8888 will still be used for this type of image  } } Copy the code

Alternatively, depending on the device score, RGB_565 can be used for regular models (ARGB_8888 is used locally for scenarios requiring transparent channels), and ARGB_8888 can be used for high-end models for a luxurious experience.


Conduct packaging

The @glideExtension annotation can wrap up some common behavior and extend an interface for easy invocation by the business layer. For example, many pages of e-commerce apps have commodity lists. If the width and height of these commodity pictures are fixed, they can be packaged.

@GlideExtension
public class MyAppExtension {
    private static final int GOODS_W = 300; // Product map width
    private static final int GOODS_H = 400; // Commodity map height

 private MyAppExtension(a) { // Private constructor  }   @GlideOption  public staticBaseRequestOptions<? > goods(BaseRequestOptions<? > options) { return options  .fitCenter()  .override(GOODS_W, GOODS_H) / wide/high  .placeholder(R.mipmap.ic_launcher) // Commodity placeholder  .error(R.mipmap.ic_launcher); // Failed to load the commodity map  } } Copy the code

Rebuild the project, to generate the build/generated/ap_generated_sources/debug/out/com/holiday/srccodestudy/glide/GlideOptions. Java, There’s an extra method in there,

class GlideOptions extends RequestOptions implements Cloneable {
    public GlideOptions goods(a) {
        return (GlideOptions) MyAppExtension.goods(this);
    }
}
Copy the code

At this point, goods can be used directly to use this set of packaged behaviors,

/ / to use GlideApp
GlideApp.with(this).load(url).goods().into(img);
Copy the code

The Generated API is more suitable for short cycle/small projects. Medium and large projects often do not use Glide directly, but package an intermediate layer for isolation (do not allow the business layer to use any of the Glide classes), so that it can be upgraded and replaced at any time, and the middle layer can be expanded as needed.

Empty Fragment Cancels the request

Glide. With (context), an empty fragment is added to each page when the context is an Activity. The empty fragment holds the page-level RequestManager to manage the request.

With through RequestManagerRetriever SupportRequestManagerFragment,

//SupportRequestManagerFragment.java
/ / create SupportRequestManagerFragment
public SupportRequestManagerFragment(a) {
    / / create a Lifecycle
    this(new ActivityFragmentLifecycle());
}  //RequestManager.java // Create the RequestManager, passing in Lifecycle RequestManager(  Glide glide,  Lifecycle lifecycle,  / /...  Context context) {  //lifecycle adds the RequestManager to the observer  lifecycle.addListener(this); }  //ActivityFragmentLifecycle.java public void addListener(LifecycleListener listener) {  // Record the observers  lifecycleListeners.add(listener); } Copy the code

When you exit the page,

//SupportRequestManagerFragment.java
public void onDestroy(a) {
    lifecycle.onDestroy();
}

//ActivityFragmentLifecycle.java void onDestroy(a) {  for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {  lifecycleListener.onDestroy();  } }  //RequestManager.java public synchronized void onDestroy(a) {  // All kinds of cancel and cancel operations  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

The code looks a little convoluted, as shown below,


Cache Cache

memory

The memory cache has two levels. One is the cache that is active and being used by the view. Second, if it’s not being used by the view, call it an inactive resource,

Read memory:

//Engine.java
public <R> LoadStatus load(...).{
    // Get the memory cache
    memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
}
 privateEngineResource<? > loadFromMemory( EngineKey key, boolean isMemoryCacheable, long startTime) {  // ActiveResources, retrieved from ActiveResources Map  //Map
               
                 activeEngineResources, the value is weak reference, will be counted manually
               ,> EngineResource<? > active = loadFromActiveResources(key); if(active ! =null) {  return active;  }  // Inactive resources, obtained from LruResourceCache, also have manual count  // The cache is used by the view, and the inactive resource becomes active EngineResource<? > cached = loadFromCache(key); if(cached ! =null) {  return cached;  }  // Memory is not cached, load will request  return null; } Copy the code

Write to memory:

//Engine.java
public synchronized void onEngineJobComplete(
EngineJob
          engineJob, Key key, EngineResource
          resource) {
    if(resource ! =null && resource.isMemoryCacheable()) {
        // When the image is loaded, write to the active resource
 activeResources.activate(key, resource);  } }  public void onResourceReleased(Key cacheKey, EngineResource
                 resource) {  // The active resource is no longer referenced  activeResources.deactivate(cacheKey);  if (resource.isMemoryCacheable()) {  // Go to an inactive resource  cache.put(cacheKey, resource);  } } Copy the code

The diagram below:


disk

Look at the cache directory/data/data/com. Holiday. Srccodestudy/cache/image_manager_disk_cache /,


Let’s look at the log file journal,

Libcore.io.DiskLruCache // Header name1 // Disk cache version1 / / App version1 // Number of files for each entry (log entry). The default value is 1, that is, one entry corresponds to one image file. For example, there are four entries, that is, four images
DIRTY 64d4b00d8ce8b0942d53b3048d5cf6aaa7173acd321e17420891bbc35b98629f CLEAN 64d4b00d8ce8b0942d53b3048d5cf6aaa7173acd321e17420891bbc35b98629f 5246 DIRTY 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37 CLEAN 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37 404730 READ 64d4b00d8ce8b0942d53b3048d5cf6aaa7173acd321e17420891bbc35b98629f READ 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37 DIRTY b566e62aa0e2fb8cb219ad3aa7a0ade9a96521526501ccd775d70aa4f6489272 CLEAN b566e62aa0e2fb8cb219ad3aa7a0ade9a96521526501ccd775d70aa4f6489272 9878 READ 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37 READ b566e62aa0e2fb8cb219ad3aa7a0ade9a96521526501ccd775d70aa4f6489272 DIRTY 55f4af9c1020e3272ce8063c351aff3518f3a1c9508f38345eab27686e263a4c CLEAN 55f4af9c1020e3272ce8063c351aff3518f3a1c9508f38345eab27686e263a4c 69284  Copy the code

Second part is the operation records and line refers to the practice in the beginning, DIRTY said in the edit (in a state of DIRTY data, don’t READ), CLEAN (CLEAN) to write good, can READ, READ is READ into the, REMOVE is said to be deleted, long a string of characters is cache key or file name, file size, in the final number For example, if 404730 B is 395.2 KB, the size is written only in the CLEAN state. So what does the file name in the picture mean, and why does the key have a.0 suffix? An entry (log entry) can correspond to multiple images.0 represents the first image of an entry. If 1 is configured for many, suffixes such as.1 and. Right click on a.0 file, Save as to your computer, change the JPG suffix, and you can see the image.

Go to the DiskLruCache class (see name or least recently used algorithm),

//DiskLruCache.java

// Order Map to implement the least recently used algorithm
private final LinkedHashMap<String, Entry> lruEntries =
    new LinkedHashMap<String, Entry>(0.0.75 f.true);
 // Read the disk cache public synchronized Value get(String key) throws IOException {  // Find entry based on key  Entry entry = lruEntries.get(key);  if (entry == null) {  return null;  }  // Not readable yet, returns null  if(! entry.readable) { return null;  }  // Append a line of log: READ  journalWriter.append(READ);  journalWriter.append(' ');  journalWriter.append(key);  journalWriter.append('\n');  //Value is the entity to encapsulate  return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths); }  // Write to disk cache (this is just a Map stored in memory, the real write is in DiskLruCacheWrapper) private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {  Entry entry = lruEntries.get(key);  if (entry == null) {  entry = new Entry(key);  / / into LinkedHashMap  lruEntries.put(key, entry);  }  Editor editor = new Editor(entry);  entry.currentEditor = editor;  // Add a line of logs: DIRTY  journalWriter.append(DIRTY);  return editor; }  // Delete the disk cache public synchronized boolean remove(String key) throws IOException {  Entry entry = lruEntries.get(key);  if (entry == null|| entry.currentEditor ! =null) {  return false;  }  // Delete the image file corresponding to entry  for (int i = 0; i < valueCount; i++) {  File file = entry.getCleanFile(i);  size -= entry.lengths[i];  entry.lengths[i] = 0;  }  // Add a log line: REMOVE  journalWriter.append(REMOVE);  // Remove from memory Map  lruEntries.remove(key);  return true; }  // When the number of log operands and entries reaches 2000, the log is cleared and rewritten private boolean journalRebuildRequired(a) {  final int redundantOpCompactThreshold = 2000;  return redundantOpCount >= redundantOpCompactThreshold //  && redundantOpCount >= lruEntries.size(); } Copy the code

So where are the read and write times? We trace back a wave of GET methods from DiskLruCache to DiskLruCacheWrapper’s GET, and then back again. We find two classes that call GET: DataCacheGenerator and ResourceCacheGenerator. The former is the cache of the original images, and the latter is the images downsampled or transformed by DownSampled or transformed by Transformed by Downsampled or transformed by Transformed by Downsampled or transformed by Transformed.

Currently supported policies allow you to prevent the loading process from using or writing to disk cache, optionally caching only unmodified native data, or only altered thumbnails, or both.

By default, the network image caches raw data, so we continue with the DataCacheGenerator,

//DataCacheGenerator.java
public boolean startNext(a) {
    while (modelLoaders == null| |! hasNextModelLoader()) {        sourceIdIndex++;
        if (sourceIdIndex >= cacheKeys.size()) {
 return false;  }  Key sourceId = cacheKeys.get(sourceIdIndex);  Key originalKey = new DataCacheKey(sourceId, helper.getSignature());  // Get the image file cached on disk  cacheFile = helper.getDiskCache().get(originalKey);  if(cacheFile ! =null) {  this.sourceKey = sourceId;  // Get a collection of modelLoaders that can handle the File type,  //modelLoader is the image loading type, such as network URL, local Uri, File have their own loader  modelLoaders = helper.getModelLoaders(cacheFile);  modelLoaderIndex = 0;  }  }  loadData = null;  boolean started = false;  while(! started && hasNextModelLoader()) { // ByteBufferFileLoader is successfully found, which can process File ModelLoader<File, ? > modelLoader = modelLoaders.get(modelLoaderIndex++); // Pass the image file to the disk cache cacheFile  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

Behind to continue with modelLoader buildLoadData, cacheFile encapsulated into ByteBufferFetcher is the image file, and then call the above loadData. The fetcher. LoadData callback, not to continue with the, The startNext method is called in the DecodeJob, which is a Runnable for the image loading process.

//DiskLruCacheWrapper.java
public void put(Key key, Writer writer) {
    String safeKey = safeKeyGenerator.getSafeKey(key);
    writeLocker.acquire(safeKey);
    try {
 try {  DiskLruCache diskCache = getDiskCache();  Value current = diskCache.get(safeKey);  // There is already a cache, end  if(current ! =null) {  return;  }  / / get the Editor  DiskLruCache.Editor editor = diskCache.edit(safeKey);  try {  File file = editor.getFile(0);  if (writer.write(file)) {// Encode to write file  // Submit "transaction", append a line of log: CLEAN, indicating that the cache file corresponding to the entry is CLEAN and ready to use  editor.commit();  }  } finally {  editor.abortUnlessCommitted();  }  } catch (IOException e) {  }  } finally {  writeLocker.release(safeKey);  } } Copy the code

Similarly, the put method is called in the DecodeJob instead of being followed.


Merge memory cache and disk cache,


BitmapPool has its critics

Glide pools bitmaps, default is LruBitmapPool, and it decides how to reuse bitmaps, when to recycle them, and when to clean up the pool when it reaches its limit. That is, it takes over the processing of bitmaps. If a project has a scenario in which a Bitmap is held outside the callback method and a Bitmap is manually reclaimed, unexpected crashes may occur. For details, see signs of resource reuse errors. That is, we have to have the awareness that since Glide is used, we don’t care about Bitmap anymore, and let BitmapPool manage everything.

Divergence: Pooling is the meta-sharing pattern in the design pattern, that is, to maintain a finite number of object pools to achieve object reuse, so as to avoid frequent creation and destruction of objects. Message.obtain, for example, takes objects from a Message pool (linked list) to be reused, and the total number of messages in the pool is limited to MAX_POOL_SIZE=50. Many implementations in Android are based on Handler (message-driven), and pooling can reduce creation and destruction by a significant amount.

Decoder to decode

The link is a little long, so if YOU look at the call stack,


It can be seen that the native layer of nativeDecodeStream is finally followed, Hardy will not follow, inputStream into bitmap readers interested in their own research ~


conclusion

Glide has the following advantages:

  1. An empty Fragment senses the page life cycle and avoids invalid requests
  2. The height is configurable. For details, see Configuration
  3. Level 3 caches (network layer caches such as OKHTTP are not considered) : memory active resourcesActiveResources, memory is an inactive resourceLruResourceCache, disk cachingDiskLruCache
  4. Customizable, apt processing annotation, packaging behavior, extended interface. (Hardy didn’t use it very much, it felt a little chicken, maybe it will smell good later)
  5. Extensible, you can replace the network layer and customize your own image sourcesModelLoader, as shown in theWrite a custom ModelLoader
  6. Non-intrusive, into can pass in the simplest ImageView
  7. Excellent design pattern application, application layer elegant chain call

As for the disadvantages, I haven’t thought of them yet. This article only lists the details that Hardy found fascinating, and there may be some missing points. If you have any additions, please leave a comment and I will update this article later.

The resources

  • Official documentation & GitHub


This article is formatted using MDNICE