B standing video 100 sets the collection of the Android source code parsing Retrofit/OkHttp/Glide/RxJava/EventBus… 】 : www.bilibili.com/video/BV1mT…

preface

Android cache mechanism: if there is no cache, in a large number of network requests from the remote image will cause network traffic waste, slow loading speed, poor user experience;

Today we are going to talk about Glide’s caching mechanism

A brief introduction to the concept of Cache in Glide

Glide split it into two modules, one memory cache and one hard disk cache.

1. Memory cache

  • The memory cache is divided into two levels: LruCache cache and weak reference cache
  • Memory cache is used to prevent applications from reading image data into memory repeatedly.
  • LruCache cache: Images that are not in use are cached using LruCache.
  • Weak reference cache: Images in use are cached with weak references to protect resources in use from being reclaimed by LruCache algorithm.

2. Hard disk cache

The functions of the disk cache are as follows: Prevents applications from repeatedly downloading and reading data from the network or other places.

3. Picture request steps

Check the following levels of caches before starting a new image request:

  • Memory cache: Has this image been recently loaded and still exists in memory? LruCache cache;
  • Activity Resources: Is there another View showing this image right now? Weak reference caching;
  • 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.);
  • Images are stored in the following order: weak reference, memory, disk;
  • The order of images is: memory, weak reference, disk;

4. Bitmap reuse mechanism in Glide

  • Bitmap reuse mechanism: Reuse data space that is no longer needed to reduce memory jitter (refers to the phenomenon that a large number of objects are created or recycled in a short time);
  • BitmapFactory. Options. InMutable Glide can reuse is the cornerstone of Bitmap, is to provide a parameter, BitmapFactory said the Bitmap is variable, and support for reuse. Bitmapfactory. Options provides two properties: inMutable and inBitmap. When multiplexing bitmaps, set inMutable to true. InBitmap sets existing bitmaps to be multiplexed. Bitmap reuse pool is realized by LRU algorithm.

Second, cache source code process

The memory cache and disk cache are also created when Glide is created. The code created by Glide is used in the glideBuilder.build (Context) method

@NonNull Glide build(@NonNull Context context) { if (memoryCache == null) { memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize()); } if (diskCacheFactory == null) { diskCacheFactory = new InternalCacheDiskCacheFactory(context); } if (engine == null) { engine = new Engine( memoryCache, diskCacheFactory, ...) ; } return new Glide( ... memoryCache, ...) ; }Copy the code

1. MemoryCache -memoryCache

You can see from the code that memoryCache is put into the Engine and Glide instances. A memoryCache is used in Engine for access operations, and it is used in Glide’s instance to notify the memoryCache to free memory when memory is running low. Glide implements ComponentCallbacks2 interface, create complete, on the Glide through the applicationContext. RegisterComponentCallbacks (Glide) like Glide instance can monitor memory signals of tension.

// Glide
@Override
public void onTrimMemory(int level) {
  trimMemory(level);
}
public void trimMemory(int level) {
  // Engine asserts this anyway when removing resources, fail faster and consistently
  Util.assertMainThread();
  // 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);
}
Copy the code

MemoryCache is a memoryCache class LruResourceCache implemented using the least recently used (LRU) algorithm. It is inherited from LruCache and implements the memoryCache interface. LruCache defines the operations that the LRU algorithm implements, while MemoryCache defines the operations that the MemoryCache implements.

LruCache is implemented using a feature of the data structure of the LinkedHashMap (accessOrder = true based on accessOrder) plus a caching strategy that locks the data operations of the LinkedHashMap.

When the put() method is called, it adds elements to the collection and calls

TrimToSize () determines if the cache is full, and if it is, uses the iterator of LinkedHashMap to remove the last element of the queue, which is the least recently accessed element.

When the get() method is called to access the cache object, the Get () method of the LinkedHashMap is called to retrieve the collection element and the element is updated to the queue header

2. Disk caching

DiskCacheFactory is the Factory that creates DiskCache. DiskCache interface is defined

public interface DiskCache {
  interface Factory {
    /** 250 MB of cache. */
    int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;
    String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache";
    @Nullable
    DiskCache build();
  }
  interface Writer {
    boolean write(@NonNull File file);
  }
  @Nullable
  File get(Key key);
  void put(Key key, Writer writer);
  @SuppressWarnings("unused")
  void delete(Key key);
  void clear();
}
Copy the code

Then look at DiskCache. The Factory default implementation: InternalCacheDiskCacheFactory

public final class InternalCacheDiskCacheFactory extends DiskLruCacheFactory { public InternalCacheDiskCacheFactory(Context context) { this(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE); } public InternalCacheDiskCacheFactory(Context context, long diskCacheSize) { this(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, diskCacheSize); } public InternalCacheDiskCacheFactory(final Context context, final String diskCacheName, long diskCacheSize) { super(new CacheDirectoryGetter() { @Override public File getCacheDirectory() { File cacheDirectory  = context.getCacheDir(); if (cacheDirectory == null) { return null; } if (diskCacheName ! = null) { return new File(cacheDirectory, diskCacheName); } return cacheDirectory; } }, diskCacheSize); }}Copy the code

As can be seen from the above code, a 250 MB cache directory is created by default. The path is /data/data/{package}/cache/image_manager_disk_cache/

Continue with the code for its parent class, DiskLruCacheFactory

public class DiskLruCacheFactory implements DiskCache.Factory { private final long diskCacheSize; private final CacheDirectoryGetter cacheDirectoryGetter; public interface CacheDirectoryGetter { File getCacheDirectory(); }... public DiskLruCacheFactory(CacheDirectoryGetter cacheDirectoryGetter, long diskCacheSize) { this.diskCacheSize = diskCacheSize; this.cacheDirectoryGetter = cacheDirectoryGetter; } @Override public DiskCache build() { File cacheDir = cacheDirectoryGetter.getCacheDirectory(); if (cacheDir == null) { return null; } if (! cacheDir.mkdirs() && (! cacheDir.exists() || ! cacheDir.isDirectory())) { return null; } return DiskLruCacheWrapper.create(cacheDir, diskCacheSize); }}Copy the code

DiskLruCacheFactory. The build () method returns a DiskLruCacheWrapper instances of the class, look at the implementation of the DiskLruCacheWrapper

public class DiskLruCacheWrapper implements DiskCache { private static final String TAG = "DiskLruCacheWrapper"; private static final int APP_VERSION = 1; private static final int VALUE_COUNT = 1; private static DiskLruCacheWrapper wrapper; private final SafeKeyGenerator safeKeyGenerator; private final File directory; private final long maxSize; private final DiskCacheWriteLocker writeLocker = new DiskCacheWriteLocker(); private DiskLruCache diskLruCache; @SuppressWarnings("deprecation") public static DiskCache create(File directory, long maxSize) { return new DiskLruCacheWrapper(directory, maxSize); } @Deprecated @SuppressWarnings({"WeakerAccess", "DeprecatedIsStillUsed"}) protected DiskLruCacheWrapper(File directory, long maxSize) { this.directory = directory; this.maxSize = maxSize; this.safeKeyGenerator = new SafeKeyGenerator(); } private synchronized DiskLruCache getDiskCache() throws IOException { if (diskLruCache == null) { diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize); } return diskLruCache; } @Override public File get(Key key) { String safeKey = safeKeyGenerator.getSafeKey(key); File result = null; try { final DiskLruCache.Value value = getDiskCache().get(safeKey); if (value ! = null) { result = value.getFile(0); } } catch (IOException e) { ... } return result; } @Override 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); . DiskLruCache.Editor editor = diskCache.edit(safeKey); . try { File file = editor.getFile(0); if (writer.write(file)) { editor.commit(); } } finally { editor.abortUnlessCommitted(); } } catch (IOException e) { ... } } finally { writeLocker.release(safeKey); }}... }Copy the code

This class provides a SafeKeyGenerator for DiskLruCache to generate a safeKey based on a Key and write lock DiskCacheWriteLocker.

Back in glideBuilder.build (Context), diskCacheFactory is passed to the Engine and wrapped as a LazyDiskCacheProvider in the constructor of the Engine. The getDiskCache() method is called when needed, which calls the factory build() method to return a DiskCache. The code is as follows:

private static class LazyDiskCacheProvider implements DecodeJob.DiskCacheProvider { private final DiskCache.Factory factory; private volatile DiskCache diskCache; LazyDiskCacheProvider(DiskCache.Factory factory) { this.factory = factory; }... @Override public DiskCache getDiskCache() { if (diskCache == null) { synchronized (this) { if (diskCache == null) { diskCache = factory.build(); } if (diskCache == null) { diskCache = new DiskCacheAdapter(); } } } return diskCache; }}Copy the code

The LazyDiskCacheProvider is passed as an input to the constructor of the DecodeJobFactory during the initialization process following the Engine. When DecodeJobFactory creates DecodeJob, it will also be passed in as an entry. The DecodeJob will save the LazyDiskCacheProvider as a global variable. After the resource is loaded and displayed, it will be cached. DecodeJob also sets the DiskCacheProvider to the cache when DecodeHelper is initialized for ResourceCacheGenerator and DataCacheGenerator to read. The SourceGenerator writes to the cache

3、 ActiveResources

ActiveResources is created in the Engine constructor, which starts a thread with a background priority level (THREAD_PRIORITY_BACKGROUND), In this thread, the cleanReferenceQueue() method is called to loop through cleaning resources in the ReferenceQueue that will be GC.

final class ActiveResources { private final boolean isActiveResourceRetentionAllowed; private final Executor monitorClearedResourcesExecutor; @VisibleForTesting final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>(); private final ReferenceQueue<EngineResource<? >> resourceReferenceQueue = new ReferenceQueue<>(); private volatile boolean isShutdown; 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() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); r.run(); } }, "glide-active-resources"); }})); } @VisibleForTesting ActiveResources( boolean isActiveResourceRetentionAllowed, Executor monitorClearedResourcesExecutor) { this.isActiveResourceRetentionAllowed = isActiveResourceRetentionAllowed; this.monitorClearedResourcesExecutor = monitorClearedResourcesExecutor; monitorClearedResourcesExecutor.execute( new Runnable() { @Override public void run() { cleanReferenceQueue(); }}); } @SuppressWarnings("WeakerAccess") @Synthetic void cleanReferenceQueue() { while (! isShutdown) { try { ResourceWeakReference ref = (ResourceWeakReference) resourceReferenceQueue.remove(); cleanupActiveReference(ref); // This section for testing only. DequeuedResourceCallback current = cb; if (current ! = null) { current.onResourceDequeued(); } // End for testing only. } catch (InterruptedException e) { Thread.currentThread().interrupt(); }}}}Copy the code

Take a look at the activate methods (save) and deactivate methods (delete) of ActiveResources

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(); }}Copy the code

The Activate method encapsulates the parameter as a ResourceWeakReference and puts it in the map. If the corresponding key has a value before, the reset method is called to clear it. The deactivate method is removed from the map and then cleared by calling the reset method of resource. ResourceWeakReference inherits WeakReference, and internally only saves some properties of Resource.

static final class ResourceWeakReference extends WeakReference<EngineResource<? >> { @SuppressWarnings("WeakerAccess") @Synthetic final Key key; @SuppressWarnings("WeakerAccess") @Synthetic final boolean isCacheable; @Nullable @SuppressWarnings("WeakerAccess") @Synthetic Resource<? > resource; @Synthetic @SuppressWarnings("WeakerAccess") ResourceWeakReference( @NonNull Key key, @NonNull EngineResource<? > referent, @NonNull ReferenceQueue<? super EngineResource<? >> queue, boolean isActiveResourceRetentionAllowed) { super(referent, queue); this.key = Preconditions.checkNotNull(key); this.resource = referent.isCacheable() && isActiveResourceRetentionAllowed ? Preconditions.checkNotNull(referent.getResource()) : null; isCacheable = referent.isCacheable(); }}Copy the code

The constructor calls super(Referent, queue), which makes the object to be GC placed in the ReferenceQueue. And ActiveResources. CleanReferenceQueue () method will always try to obtain from the queue to be the resource of GC, Then call the cleanupActiveReference method to remove the Resource from active Resources. CleanupActiveReference source code:

void cleanupActiveReference(@NonNull ResourceWeakReference ref) { synchronized (listener) { synchronized (this) { // Remove the active resources activeEngineResources. Remove (ref. Key); if (! ref.isCacheable || ref.resource == null) { return; } // Create a new Resource EngineResource<? > newResource = new EngineResource<>(ref.resource, /*isCacheable=*/ true, /*isRecyclable=*/ false); newResource.setResourceListener(ref.key, listener); / / / / callback Engine onResourceReleased method which leads to the resources from the active to the memory cache state listener. OnResourceReleased (ref. Key, newResource); }}}Copy the code

Engine implements EngineResource ResourceListener, the listener is Engine, here will eventually callback Engine. OnResourceReleased

@Override
  public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    activeResources.deactivate(cacheKey);
    if (resource.isCacheable()) {
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource);
    }
  }
Copy the code

If the resource can be cached, it is cached in memory cache. Otherwise, the resource is reclaimed

4. Read from disk cache

Let’s look at the cache access code. We look at the

public synchronized <R> LoadStatus load(...) { EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource<? > active = loadFromActiveResources(key, isMemoryCacheable); if (active ! = null) { cb.onResourceReady(active, DataSource.MEMORY_CACHE); return null; } EngineResource<? > cached = loadFromCache(key, isMemoryCacheable); if (cached ! = null) { cb.onResourceReady(cached, DataSource.MEMORY_CACHE); return null; } EngineJob<? > current = jobs.get(key, onlyRetrieveFromCache); if (current ! = null) { current.addCallback(cb, callbackExecutor); return new LoadStatus(cb, current); } EngineJob<R> engineJob = engineJobFactory.build(...) ; DecodeJob<R> decodeJob = decodeJobFactory.build(...) ; jobs.put(key, engineJob); engineJob.addCallback(cb, callbackExecutor); engineJob.start(decodeJob); return new LoadStatus(cb, engineJob); }Copy the code

The cache needs to be accessed by the EngineKey. Let’s look at how the EngineKey is constructed

EngineKey( Object model, Key signature, int width int height, Map<Class<? >, Transformation<? >> transformations, Class<? > resourceClass, Class<? > transcodeClass, Options options)Copy the code
  • Model: parameters passed by the load method;
  • Equestoptions member variable. Default is EmptySignature. Obtain ().
  • When loading the local resource resources into ApplicationVersionSignature. Obtain (context);
  • Width, height: If override(int size) is not specified, the view size is obtained;
  • You can set four transformations based on the scaleType of ImageView by default.
  • If a transform is specified, it is set based on that value;
  • ResourceClass: Decoded resource. If there is no asBitmap or asGif, it is usually Object.
  • TranscodeClass: the data type to be converted to, as determined by the AS method. When loading a local RES or a network URL, asDrawable is called
  • Options: If the transform is not set, an option will be specified based on the ImageView scaleType by default.
  • Therefore, when loading the same model multiple times, if any of the above parameters change, it will not be considered the same key;

Return to the engine.load method and call cb.onResourceready (cached, datasource.memory_cache) after a successful load from the cache; You can see: The EngineResource object uses a reference count to determine whether the resource has been released. If the reference count is 0, the EngineResource object uses a reference count to determine whether the resource has been released. Then invokes the listener. OnResourceReleased (key, this, this resource has released method to inform the outside world. The listener is a ResourceListener interface and has only one onResourceReleased(Key Key, EngineResource<? > resource). Engine implements this interface, and the listener here is Engine. In the Engine. The onResourceReleased method will determine whether resources can cache, cacheable this resource in the cache memory, or recycling the resources, the code is as follows:

public synchronized void onResourceReleased(Key cacheKey, EngineResource<? > resource) {/ / removed from the activeResources activeResources deactivate (cacheKey); IsCacheable () {// Store MemoryCache cache.put(cacheKey, resource); } else { resourceRecycler.recycle(resource); }}Copy the code

Going back to the engine.load method, let’s take a look at the active resource fetch method

@Nullable private EngineResource<? > loadFromActiveResources(Key Key, Boolean isMemoryCacheable) {// Set skipMemoryCache(true), isMemoryCacheable to false, Skip ActiveResources if (! isMemoryCacheable) { return null; } EngineResource<? > active = activeResources.get(key); if (active ! = null) {// Hit cache, reference count +1 active.acquire(); } return active; }Copy the code

Continue to analyze how cached resources are fetched, and if not from the active resource, continue to look in the in-memory cache

private EngineResource<? > loadFromCache(Key Key, Boolean isMemoryCacheable) {// Set skipMemoryCache(true), isMemoryCacheable to false, Skip ActiveResources if (! isMemoryCacheable) { return null; } EngineResource<? > cached = getEngineResourceFromCache(key); if (cached ! = null) {// Hit cache, reference count +1 cached.acquire(); // Move the resource from memoryCache to activeresources. activate(key, cached); } return cached; }Copy the code

If a resource is fetched from memoryCache, it is moved from memoryCache to activeResources. ActiveResources and memoryCache are not cached when first loaded, and then resources are loaded using DecodeJob and EngineJob. DecoceJob implements the Runnable interface and is submitted to the corresponding thread pool for execution by enginejob. start. In the Run method of DecoceJob, cache data is fetched from ResourceCacheGenerator and DataCacheGenerator in turn, and when neither is available, it is handed over to SourceGenerator to load network images or local resources. Resource resources and data resources are both resources in the disk cache.

Look at the first ResourceCacheGenerator startNext

@override public Boolean startNext() {list <Key> sourceIds = helper.getcacheKeys (); if (sourceIds.isEmpty()) { return false; } / / won three can reach registeredResourceClasses / / GifDrawable, Bitmap, BitmapDrawable List < Class <? >> resourceClasses = helper.getRegisteredResourceClasses(); if (resourceClasses.isEmpty()) { if (File.class.equals(helper.getTranscodeClass())) { return false; } throw new IllegalStateException( "Failed to find any load path from " + helper.getModelClass() + " to " + helper.getTranscodeClass()); } / / traverse sourceIds every one of the key, resourceClasses in every class, and other values of key / / try to key found in the disk cache cache files while (modelLoaders = = null | |! hasNextModelLoader()) { resourceClassIndex++; if (resourceClassIndex >= resourceClasses.size()) { sourceIdIndex++; if (sourceIdIndex >= sourceIds.size()) { return false; } resourceClassIndex = 0; } Key sourceId = sourceIds.get(sourceIdIndex); Class<? > resourceClass = resourceClasses.get(resourceClassIndex); Transformation<? > transformation = helper.getTransformation(resourceClass); // PMD.AvoidInstantiatingObjectsInLoops Each iteration is comparatively expensive anyway, // we only run until the first one succeeds, The loop runs for only a limited // number of iterations on the order of 10-20 in the worst case. // Create key currentKey = new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoops helper.getArrayPool(), sourceId, helper.getSignature(), helper.getWidth(), helper.getHeight(), transformation, resourceClass, helper.getOptions()); CacheFile = helper.getDiskCache().get(currentKey); // if a cacheFile is found, the loop condition is false and exits the loop if (cacheFile! = null) { sourceKey = sourceId; // 1\. Find the injection code with file. class as the modelClass. // 2\. Call all injected factory.build methods to get ModelLoader // 3. Filter out the modelLoaders that cannot process the model.  // [ByteBufferFileLoader, FileLoader, FileLoader, UnitModelLoader] modelLoaders = helper.getModelLoaders(cacheFile); modelLoaderIndex = 0; } // If the cache file is found, the hasNextModelLoader() method will be true and the loop can be executed. boolean started = false; while (! started && hasNextModelLoader()) { ModelLoader<File, ? > modelLoader = modelLoaders.get(modelLoaderIndex++); / / in a loop will, in turn, determine whether a ModelLoader can load the file loadData. = ModelLoader buildLoadData (cacheFile, helper. GetWidth (), helper.getHeight(), helper.getOptions()); if (loadData ! = null && helper.hasLoadPath(loadData.fetcher.getDataClass())) { started = true; // If a ModelLoader is available, call its fetcher to load the dataCopy the code
/ / load the success or failure will inform their loadData. The fetcher. LoadData (helper, getPriority (), this); } } return started; }Copy the code

This method is indicated in the associated comment code. The type of the key used to find the cache is ResourceCacheKey. Let’s look at the composition of ResourceCacheKey

currentKey = new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoops helper.getArrayPool(), sourceId, helper.getSignature(), helper.getWidth(), helper.getHeight(), transformation, resourceClass, helper.getOptions()); ResourceCacheKey( ArrayPool arrayPool, Key sourceKey, Key signature, int width, int height, Transformation<? > appliedTransformation, Class<? > decodedResourceClass, Options options)Copy the code
  • ArrayPool: The default value is LruArrayPool and does not participate in the equals method of the key;
  • SourceKey: GlideUrl (GlideUrl implements Key);
  • Equestoptions member variable. Default is EmptySignature. Obtain ().
  • When loading the local resource resources into ApplicationVersionSignature. Obtain (context);
  • Width, height: If override(int size) is not specified, the view size is obtained;
  • AppliedTransformation: The default BitmapTransformation is set based on the ImageView scaleType.
  • If a transform is specified, it will be the specified value;
  • DecodedResourceClass: resource type that can be encoded, such as BitmapDrawable, etc.
  • Options: If the transform is not set, an option will be specified based on the ImageView scaleType by default.

In ResourceCacheKey, arrayPool does not participate in equals;

CacheFile = helper.getDiskCache().get(currentKey);

Helper.getdiskcache () returns the DiskCache interface, whose implementation class is DiskLruCacheWrapper. Look at the DiskLruCacheWrapper

@Override public File get(Key key) { String safeKey = safeKeyGenerator.getSafeKey(key); . File result = null; try { final DiskLruCache.Value value = getDiskCache().get(safeKey); if (value ! = null) { result = value.getFile(0); } } catch (IOException e) { ... } return result; }Copy the code

This call to SafeKeyGenerator generates a String SafeKey, which essentially uses SHA-256 encryption for each field in the original key, and then converts the resulting byte array into a hexadecimal String. After the SafeKey is generated, the system searches for the corresponding cache file in DiskCache according to the SafeKey and returns the file.

Back to ResourceCacheGenerator startNext approach, if found the cache will be called the loadData. Fetcher. LoadData (helper. GetPriority (), this); Fetcher here is ByteBufferFetcher, The loadData method of ByteBufferFetcher eventually executes callback.onDataReady(result) where callback is ResourceCacheGenerator

  public void onDataReady(Object data) {
    cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.RESOURCE_DISK_CACHE,
        currentKey);
  }
Copy the code

The onDataReady method of ResourceCacheGenerator calls back to the onDataFetcherReady method of DecodeJob for subsequent decoding operations.

If ResourceCacheGenerator does not find the cache, it is handed over to DataCacheGenerator to continue looking for the cache. The general flow of this class is the same as ResourceCacheGenerator, except that the DataCacheGenerator constructor has two constructors, where DataCacheGenerator(List, DecodeHelper<? >, FetcherReadyCallback) constructor is for SourceGenerator. If you don’t have disk caching, you will definitely need to do disk caching after loading from source. So, The SourceGenerator will save the loaded resource to disk and hand it over to DataCacheGenerator to take it from disk and present it to ImageView.

Look at the DataCacheGenerator startNext

public boolean startNext() {
    while (modelLoaders == null || !hasNextModelLoader()) {
      sourceIdIndex++;
      if (sourceIdIndex >= cacheKeys.size()) {
        return false;
      }
      Key sourceId = cacheKeys.get(sourceIdIndex);
      ...
      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
      cacheFile = helper.getDiskCache().get(originalKey);
      ...
    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

Where originalKey is of type DataCacheKey, DataCacheKey is constructed as follows

DataCacheKey(Key sourceKey, Key signature)

DataCache caches raw data, while ResourceCache caches decoded and transformed data.

If the DataCacheGenerator does not fetch the cache, it is handed over to The SourceGenerator to load from the source. Take a look at the startNext method of SourceGenerator

@override public Boolean startNext() {// dataToCache is null if (dataToCache! = null) { Object data = dataToCache; dataToCache = null; cacheData(data); } // Run sourceCacheGenerator for the first time to null if (sourceCacheGenerator! = null && sourceCacheGenerator.startNext()) { return true; } sourceCacheGenerator = null; loadData = null; boolean started = false; while (! started && hasNextModelLoader()) { loadData = helper.getLoadData().get(loadDataListIndex++); if (loadData ! = null && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource()) || helper.hasLoadPath(loadData.fetcher.getDataClass()))) { started = true; loadData.fetcher.loadData(helper.getPriority(), this); } } return started; }Copy the code

SourceGenerator’s onDataReady method is still called back after a successful load

@Override public void onDataReady(Object data) { DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy(); if (data ! = null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) { dataToCache = data; DecodeJob cb.reschedule(); // DecodeJob cb.reschedule(); } else {DecodeJob cb.onDataFetcherReady(loadData.sourcekey, data, loadData.fetcher, DecodeJob cb.onDataFetcherReady, loadData.sourcekey, data, loadData.fetcher, loadData.fetcher.getDataSource(), originalKey); }}Copy the code

To determine whether you need access to the data on disk cache, if you need a disk cache, then after DecodeJob, EngineJob scheduling, to call the SourceGenerator. StartNext method, the dataToCache has been assigned, CacheData (data) is called; Write to disk cache and pass to DataCacheGenerator for subsequent processing; Otherwise, DecodeJob is notified that it has loaded successfully.

Look under the SourceGenerator startNext method called the SourceGenerator. CacheData (data)

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()); helper.getDiskCache().put(originalKey, writer); . } finally { loadData.fetcher.cleanup(); } sourceCacheGenerator = new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this); }Copy the code

The cacheData method first builds a DataCacheKey that writes data to disk and then assigns a new DataCacheGenerator to sourceCacheGenerator. Go back to startNext and continue with sourceCacheGenerator not empty, call its startNext() method to load the data just written to disk from disk and return true to tell DecodeJob to stop trying to fetch the data. At this point, the logic to read data from the disk cache is complete, followed by writing to the disk cache.

If the SourceGenerator onDataReady disk caching strategy in the method is not available, will callback DecodeJob. OnDataFetcherReady method

// DecodeJob @Override public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<? > fetcher, DataSource dataSource, Key attemptedKey) { this.currentSourceKey = sourceKey; this.currentData = data; this.currentFetcher = fetcher; this.currentDataSource = dataSource; this.currentAttemptingKey = attemptedKey; if (Thread.currentThread() ! = currentThread) { runReason = RunReason.DECODE_DATA; callback.reschedule(this); } else { GlideTrace.beginSection("DecodeJob.decodeFromRetrievedData"); try { decodeFromRetrievedData(); } finally { GlideTrace.endSection(); } } } private void decodeFromRetrievedData() { ... Resource<R> resource = null; try { resource = decodeFromData(currentFetcher, currentData, currentDataSource); } catch (GlideException e) { e.setLoggingDetails(currentAttemptingKey, currentDataSource); throwables.add(e); } if (resource ! = null) { notifyEncodeAndRelease(resource, currentDataSource); } else { runGenerators(); }}Copy the code

decodeFromRetrievedData(); The subsequent method call chain, which was analyzed in the previous article, basically does the following: converts raw data into resource data that can be displayed by the ImageView and displays it on the ImageView.

The original data, the data into the resource data, will call DecodeJob. OnResourceDecoded (dataSource, decoded)

@Synthetic @NonNull <Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) { @SuppressWarnings("unchecked") Class<Z> resourceSubClass = (Class<Z>) decoded.get().getClass(); Transformation<Z> appliedTransformation = null; Resource<Z> transformed = decoded; // Transform if (dataSource! = DataSource.RESOURCE_DISK_CACHE) { appliedTransformation = decodeHelper.getTransformation(resourceSubClass); transformed = appliedTransformation.transform(glideContext, decoded, width, height); } // TODO: Make this the responsibility of the Transformation. if (! decoded.equals(transformed)) { decoded.recycle(); } final EncodeStrategy encodeStrategy; final ResourceEncoder<Z> encoder; if (decodeHelper.isResourceEncoderAvailable(transformed)) { encoder = decodeHelper.getResultEncoder(transformed); encodeStrategy = encoder.getEncodeStrategy(options); } else { encoder = null; encodeStrategy = EncodeStrategy.NONE; } Resource<Z> result = transformed; boolean isFromAlternateCacheKey = ! decodeHelper.isSourceKey(currentSourceKey); if (diskCacheStrategy.isResourceCacheable(isFromAlternateCacheKey, dataSource, encodeStrategy)) { if (encoder == null) { throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass()); } final Key key; switch (encodeStrategy) { case SOURCE: key = new DataCacheKey(currentSourceKey, signature); break; case TRANSFORMED: key = new ResourceCacheKey( decodeHelper.getArrayPool(), currentSourceKey, signature, width, height, appliedTransformation, resourceSubClass, options); break; default: throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy); } LockedResource<Z> lockedResult = LockedResource.obtain(transformed); deferredEncodeManager.init(key, encoder, lockedResult); result = lockedResult; } return result; }Copy the code

Then is the process of disk cache, in the process of the influence factors are encodeStrategy, DiskCacheStrategy. IsResourceCacheable. EncodeStrategy judged by the type of resource data, if it was Bitmap or BitmapDrawable, then it was TRANSFORMED; If it’s a GifDrawable, it’s a SOURCE. Disk caching policy is the default DiskCacheStrategy. AUTOMATIC. The source code is as follows:

public static final DiskCacheStrategy AUTOMATIC = new DiskCacheStrategy() { public boolean isDataCacheable(DataSource dataSource) { return dataSource == DataSource.REMOTE; } public boolean isResourceCacheable(boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) { return (isFromAlternateCacheKey && dataSource == DataSource.DATA_DISK_CACHE || dataSource == DataSource.LOCAL) && encodeStrategy == EncodeStrategy.TRANSFORMED; } public boolean decodeCachedResource() { return true; } public boolean decodeCachedData() { return true; }};Copy the code
  • Only a dataSource for the dataSource. LOCAL and encodeStrategy for encodeStrategy. TRANSFORMED, allowed to cache. Only resources whose local resource data is a Bitmap or BitmapDrawable can be cached.
  • In DecodeJob. OnResourceDecoded will call deferredEncodeManager. Init (key, encoder, lockedResult); Deinitialize the deferredEncodeManager.

In DecodeJob decodeFromRetrievedData (); NotifyEncodeAndRelease (Resource, currentDataSource) is used to write data to the disk cache.

otifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) { ... // notifyComplete(result, dataSource) that the resource is ready; stage = Stage.ENCODE; try { if (deferredEncodeManager.hasResourceToEncode()) { deferredEncodeManager.encode(diskCacheProvider, options); } } finally { if (lockedResource ! = null) { lockedResource.unlock(); } } onEncodeComplete(); }Copy the code

DeferredEncodeManager. Encode lines written to disk cache

// DecodeJob private static class DeferredEncodeManager<Z> { private Key key; private ResourceEncoder<Z> encoder; private LockedResource<Z> toEncode; @Synthetic DeferredEncodeManager() { } // We just need the encoder and resource type to match, which this will enforce. @SuppressWarnings("unchecked") <X> void init(Key key, ResourceEncoder<X> encoder, LockedResource<X> toEncode) { this.key = key; this.encoder = (ResourceEncoder<Z>) encoder; this.toEncode = (LockedResource<Z>) toEncode; } void encode(DiskCacheProvider diskCacheProvider, Options options) { GlideTrace.beginSection("DecodeJob.encode"); Try {/ / in the disk cache diskCacheProvider. GetDiskCache (), put (key, new DataCacheWriter < > (encoder, toEncode, options)); } finally { toEncode.unlock(); GlideTrace.endSection(); } } boolean hasResourceToEncode() { return toEncode ! = null; } void clear() { key = null; encoder = null; toEncode = null; }}Copy the code

DiskCacheProvider. GetDiskCache () get to DiskLruCacheWrapper, and call the DiskLruCacheWrapper put into. DiskCacheWriteLocker is used when writing to DiskLruCacheWrapper. The lock object is created by the object pool WriteLockPool. The WriteLock implementation is an unfair ReentrantLock.

Before writing data to the cache, the system checks whether the value corresponding to the key exists. If so, the system does not write data to the cache. The actual writes to the cache are handed over by DataCacheWriter to two concrete classes, ByteBufferEncoder (which writes ByteBuffer to a file) and StreamEncoder (which writes InputStream to a file).

So far, the read and write flow of disk cache has been analyzed.

5. MemoryCache: ActiveResource and MemoryCache reads

Back to DecodeJob. NotifyEncodeAndRelease method, through the notifyComplete, EngineJob. OnResourceReady, notifyCallbacksOfResult method.

In this method, on the one hand the original resource will be packaged into a EngineResource, then through callbacks to the Engine. OnEngineJobComplete

@Override public synchronized void onEngineJobComplete( EngineJob<? > engineJob, Key key, EngineResource<? > resource) {// Set the resource's callback to its own, so that when the resource is released, its own callback method is notified if (resource! = null) { resource.setResourceListener(key, this); If (resource.iscacheable ()) {activeresources.activate (key, resource); // Add the resource to activeResources and the resource becomes active. Jobs. removeIfCurrent(key, engineJob); }Copy the code

At this point, resources are put into activeResources and become active. Later use Executors. MainThreadExecutor () call SingleRequest. OnResourceReady to resources display callback. Both acquire() and release() operations on engineResource occur at one place before and after the callback is triggered, The two operations in notifyCallbacksOfResult respectively () method of incrementPendingCallbacks, decrementPendingCallbacks () call

@Synthetic void notifyCallbacksOfResult() { ResourceCallbacksAndExecutors copy; Key localKey; EngineResource<? > localResource; synchronized (this) { ... engineResource = engineResourceFactory.build(resource, isCacheable); . hasResource = true; copy = cbs.copy(); incrementPendingCallbacks(copy.size() + 1); localKey = key; localResource = engineResource; } listener.onEngineJobComplete(this, localKey, localResource); for (final ResourceCallbackAndExecutor entry : copy) { entry.executor.execute(new CallResourceReady(entry.cb)); } decrementPendingCallbacks(); } synchronized void incrementPendingCallbacks(int count) { ... if (pendingCallbacks.getAndAdd(count) == 0 && engineResource ! = null) { engineResource.acquire(); } } synchronized void decrementPendingCallbacks() { ... int decremented = pendingCallbacks.decrementAndGet(); if (decremented == 0) { if (engineResource ! = null) { engineResource.release(); } release(); } } private class CallResourceReady implements Runnable { private final ResourceCallback cb; CallResourceReady(ResourceCallback cb) { this.cb = cb; } @Override public void run() { synchronized (EngineJob.this) { if (cbs.contains(cb)) { // Acquire for this particular callback. engineResource.acquire(); callCallbackOnResourceReady(cb); removeCallback(cb); } decrementPendingCallbacks(); }}}Copy the code

EngineResource. Acquire () is also called in the run method of CallResourceReady, and the reference count of engineResource is 1 after the above code is called. The reference count for engineResource will eventually call the Singlerequest.clear () method in the requestManager.ondeStory method, Singlerequest.clear () internally calls releaseResource() and engine.release to release the reference count to zero. When the reference count becomes zero, Engine is told to change the resource from the active state to the memory cache state. If the resource can be loaded from memory cache when reloaded, the resource will change from memory cache state to active state again. That is, after the resource is first displayed, we close the page and the resource changes from active to memory cache. Then we go to the page again, and when it loads, it hits the memory cache and becomes active again

conclusion

  • When reading the memory cache, it first reads from the memory cache of LruCache algorithm mechanism, and then reads from the memory cache of weak reference mechanism.
  • When the image is written to the memory cache, it is first written to the memory cache of the weak reference mechanism, and then written to the memory cache of the LruCache algorithm mechanism when the image is no longer in use.
  • When reading the disk cache, the cache of the converted image is read first, and then the cache of the original image is read.

B standing video 100 sets the collection of the Android source code parsing Retrofit/OkHttp/Glide/RxJava/EventBus… 】 : www.bilibili.com/video/BV1mT…