Glide's caching mechanism

Glide's cache is divided into two modules, one is memory cache and the other is hard disk cache.

The function of memory cache is to prevent duplicate data from being read;

The function of hard disk cache is to prevent applications from repeatedly downloading and reading data from the network or other places.

Cache Key of Glide

The code that generates the cache Key is in the load() method of the Engine class:

final String id = fetcher.getId();
EngineKey key = keyFactory.buildKey(id, signature, width, height, 
loadProvider.getCacheDecoder(),loadProvider.getSourceDecoder(), 
transformation, loadProvider.getEncoder(),transcoder, 
loadProvider.getSourceEncoder());

fetcher. The getid () method obtains an id string, which is the unique identification of the image we want to load.

If it is a picture on the network, this id is the url address of the picture.

Memory cache

By default, Glide automatically turns on the memory cache.

To disable memory caching:

Glide.with(this).load(url).skipMemoryCache(true).into(imageView);

Glide memory cache is implemented using LruCache

In the GlideBuilder class:

Glide createGlide() {
    if (sourceService == null) {
        final int cores = Math.max(1, Runtime.getRuntime().availableProcessors());
        sourceService = new FifoPriorityThreadPoolExecutor(cores);
    }
    if (diskCacheService == null) {
        diskCacheService = new FifoPriorityThreadPoolExecutor(1);
    }

    MemorySizeCalculator calculator = new MemorySizeCalculator(context);
    if (bitmapPool == null) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            int size = calculator.getBitmapPoolSize();
            bitmapPool = new LruBitmapPool(size);
        } else {
            bitmapPool = new BitmapPoolAdapter();
        }
    }

    if (memoryCache == null) {
        memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
    }

    if (diskCacheFactory == null) {
        diskCacheFactory = new InternalCacheDiskCacheFactory(context);
    }

    if (engine == null) {
        engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
    }

    if (decodeFormat == null) {
        decodeFormat = DecodeFormat.DEFAULT;
    }

    return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
}

Line 21: create an instance of LruResourceCache, which inherits from LruCache.

In the load() method of the Engine class:

public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
            Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
        Util.assertMainThread();
        long startTime = LogTime.getLogTime();

        final String id = fetcher.getId();
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());

        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
            cb.onResourceReady(cached);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from cache", startTime, key);
            }
            return null;
        }

        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
            cb.onResourceReady(active);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from active resources", startTime, key);
            }
            return null;
        }

        EngineJob current = jobs.get(key);
        if (current != null) {
            current.addCallback(cb);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Added to existing load", startTime, key);
            }
            return new LoadStatus(cb, current);
        }

        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority);
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable);

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
    }

As you can see, the loadFromCache() method is called in line 12 to get the cached image. If you get it, you can directly call CB The onresourceready() method performs a callback. If it is not obtained, the loadFromActiveResources() method will be called on line 21 to obtain the cached image. If it is obtained, it will also be called back directly. Only when neither method gets the cache will it continue to execute downward, so as to start the thread to load the picture.

That is, during the image loading process of Glide, two methods will be called to obtain the memory cache, loadFromCache() and loadFromActiveResources(). One of the two methods uses LruCache algorithm, and the other uses weak reference.

In the Engine class:

private final MemoryCache cache;
private final Map<Key, WeakReference<EngineResource<?>>> activeResources;

private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
            return null;
        }
        EngineResource<?> cached = getEngineResourceFromCache(key);
        if (cached != null) {
            cached.acquire();
            activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
        }
        return cached;
    }

    private EngineResource<?> getEngineResourceFromCache(Key key) {
        Resource<?> cached = cache.remove(key);
        final EngineResource result;
        if (cached == null) {
            result = null;
        } else if (cached instanceof EngineResource) {
            result = (EngineResource) cached;
        } else {
            result = new EngineResource(cached, true /*isCacheable*/);
        }
        return result;
    }

    private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
            return null;
        }
        EngineResource<?> active = null;
        WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
        if (activeRef != null) {
            active = activeRef.get();
            if (active != null) {
                active.acquire();
            } else {
                activeResources.remove(key);
            }
        }
        return active;
    }

In the loadFromCache() method, the getEngineResourceFromCache() method is called to get the cache. In this method, the cache Key will be used to get values from the cache, and the cache object here is the LruResourceCache created when building the Glide object.

In line 16, when we get the cached image from LruResourceCache, we will remove it from the cache, and then store the cached image in active resources in line 10. activeResources is a weakly referenced HashMap used to cache images in use. We can see that the loadFromActiveResources() method takes values from the HashMap of activeResources. Using active resources to cache images in use can protect these images from being recycled by LruCache algorithm.

To sum up, the logic of reading data from the memory cache. If the image to be loaded can be read from the memory cache, the callback will be carried out directly. If it cannot be read, the thread will be started to execute the following image loading logic.

The logical analysis of the memory cache is finished, and then analyze where the memory cache is written.

In onEngineJobComplete() method of Engine:

    @Override
    public void onEngineJobComplete(Key key, EngineResource<?> resource) {
        Util.assertMainThread();
        // A null resource indicates that the load failed, usually due to an exception.
        if (resource != null) {
            resource.setResourceListener(key, this);
            if (resource.isCacheable()) {
                activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
            }
        }
        jobs.remove(key);
    }

In line 8, the callback EngineResource is put into active resources, that is, the cache written here. So this is just a weak reference cache. Where is another LruCache cache written?

The reference mechanism in the EngineResource class uses an acquired variable to record the number of times the picture is referenced. Calling the acquire() method will increase the variable by 1, and calling the release() method will decrease the variable by 1. The code is as follows:

private int acquired;
    void acquire() {
        if (isRecycled) {
            throw new IllegalStateException("Cannot acquire a recycled resource");
        }
        if (!Looper.getMainLooper().equals(Looper.myLooper())) {
            throw new IllegalThreadStateException("Must call acquire on the main thread");
        }
        ++acquired;
    }

    void release() {
        if (acquired <= 0) {
            throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
        }
        if (!Looper.getMainLooper().equals(Looper.myLooper())) {
            throw new IllegalThreadStateException("Must call release on the main thread");
        }
        if (--acquired == 0) {
            listener.onResourceReleased(key, this);
        }
    }

When acquired==0, call the onResourceReleased() method of listener to release resources. Listener is an Engine object. Its onResourceReleased() method:

    @Override
    public void onResourceReleased(Key cacheKey, EngineResource resource) {
        Util.assertMainThread();
        activeResources.remove(cacheKey);
        if (resource.isCacheable()) {
            cache.put(cacheKey, resource);
        } else {
            resourceRecycler.recycle(resource);
        }
    }

As you can see, the cached image will be removed from active resources first, and then put into LruResourceCache. This also realizes the function of using weak references to cache images in use and LruCache to cache images not in use.

Summary:

1) First get the cache from LruCache. If not, get the cache from weak reference;

2) After asynchronously loading the picture, cache it to the weak reference;

3) After obtaining the cache from LruCache, remove it first, and then cache it to the weak reference;

4) When the resource is released, it is removed from the weak reference and then stored in LruCache.

Hard disk cache

Disable hard disk caching:

Glide.with(this).load(url).diskCacheStrategy(DiskCacheStrategy.NONE).into(imageView);

The implementation of hard disk cache also uses the LruCache algorithm. Glide uses the DiskLruCache tool class written by itself, but the basic implementation principle is the same.

Glide starts the thread to load the picture, and will execute the run() method of EngineRunnable. In the run() method, another decode() method will be called:

private Resource<?> decode() throws Exception {
    if (isDecodingFromCache()) {
        return decodeFromCache();
    } else {
        return decodeFromSource();
    }
}

As you can see, there are two situations here: one is to call the decodeFromCache() method to read the picture from the hard disk cache, and the other is to call the decodeFromSource() method to read the original picture. By default, Glide will preferentially read from the cache. Only when there is no picture to be read in the cache will it read the original picture. Source code of the decodeFromCache() method:

private Resource<?> decodeFromCache() throws Exception {
    Resource<?> result = null;
    try {
        result = decodeJob.decodeResultFromCache();
    } catch (Exception e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Exception decoding result from cache: " + e);
        }
    }
    if (result == null) {
        result = decodeJob.decodeSourceFromCache();
    }
    return result;
}

You can see that here, you will first call the decodeResultFromCache() method of DecodeJob to get the cache. If you can't get it, you will call the decodeSourceFromCache() method to get the cache. The difference between the two methods is actually diskcachestrategy Result and diskcachestrategy Source the difference between these two parameters.

public Resource<Z> decodeResultFromCache() throws Exception {
    if (!diskCacheStrategy.cacheResult()) {
        return null;
    }
    long startTime = LogTime.getLogTime();
    Resource<T> transformed = loadFromCache(resultKey);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    return result;
}

public Resource<Z> decodeSourceFromCache() throws Exception {
    if (!diskCacheStrategy.cacheSource()) {
        return null;
    }
    long startTime = LogTime.getLogTime();
    Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
    return transformEncodeAndTranscode(decoded);
}

It can be seen that they all call the loadFromCache() method to read data from the cache. If it is the decodeResultFromCache() method, the data will be decoded and returned directly. If it is the decodeSourceFromCache() method, they also call the transformEncodeAndTranscode() method to convert the data first, and then decode and return.

It should be noted that the parameters passed in when calling the loadFromCache() method in the two methods are different. One passes in the resultKey, and the other calls the getOriginalKey() method of the resultKey.

Next, look at the source code of the loadFromCache() method:

private Resource<T> loadFromCache(Key key) throws IOException {
    File cacheFile = diskCacheProvider.getDiskCache().get(key);
    if (cacheFile == null) {
        return null;
    }
    Resource<T> result = null;
    try {
        result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
    } finally {
        if (result == null) {
            diskCacheProvider.getDiskCache().delete(key);
        }
    }
    return result;
}

The getDiskCache() method gets the instance of the DiskLruCache tool class written by Glide, then calls its get() method and sends the cached Key into it to get the file that the hard disk caches. If the file is empty, null is returned. If the file is not empty, it can be decoded into a Resource object and returned.

Then analyze where the hard disk cache is written. When there is no cache, the decodeFromSource() method is called to read the original image.

public Resource<Z> decodeFromSource() throws Exception {
    Resource<T> decoded = decodeSource();
    return transformEncodeAndTranscode(decoded);
}

Decodesource () is used to parse the original image, while transformEncodeAndTranscode() is used to convert and transcode the image. First look at the decodeSource() method:

private Resource<T> decodeSource() throws Exception {
    Resource<T> decoded = null;
    try {
        long startTime = LogTime.getLogTime();
        final A data = fetcher.loadData(priority);
        if (isCancelled) {
            return null;
        }
        decoded = decodeFromSourceData(data);
    } finally {
        fetcher.cleanup();
    }
    return decoded;
}

private Resource<T> decodeFromSourceData(A data) throws IOException {
    final Resource<T> decoded;
    if (diskCacheStrategy.cacheSource()) {
        decoded = cacheAndDecodeSourceData(data);
    } else {
        long startTime = LogTime.getLogTime();
        decoded = loadProvider.getSourceDecoder().decode(data, width, height);
    }
    return decoded;
}

private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
    long startTime = LogTime.getLogTime();
    SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
    diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
    startTime = LogTime.getLogTime();
    Resource<T> result = loadFromCache(resultKey.getOriginalKey());
    return result;
}

In line 5, first call the fetcher's loadData() method to read the picture data, and then call the decodeFromSourceData() method in line 9 to decode the picture.

Next, in line 18, you will first judge whether to allow caching of the original image. If so, you will call the cacheAndDecodeSourceData() method. In this method, the getDiskCache() method is also called to obtain the DiskLruCache instance, and then its put() method is called to write to the hard disk cache. Note that the cache Key of the original image is the resultkey getOriginalKey().

Next, analyze the source code of the transformEncodeAndTranscode() method to see how the converted image cache is written:

private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
    long startTime = LogTime.getLogTime();
    Resource<T> transformed = transform(decoded);
    writeTransformedToCache(transformed);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    return result;
}

private void writeTransformedToCache(Resource<T> transformed) {
    if (transformed == null || !diskCacheStrategy.cacheResult()) {
        return;
    }
    long startTime = LogTime.getLogTime();
    SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
    diskCacheProvider.getDiskCache().put(resultKey, writer);
}

First, the transform() method is used to convert the picture in the third row, then the converted image is written to the hard disk cache in the writeTransformedToCache() method, and the put() method is also called the DiskLruCache instance, but the cache Key used here is resultKey.

Summary:

1) Get from the hard disk cache, first get the processed picture, and then get the original picture if it can't be obtained

2) When there is no cache, the original image will be obtained. After downloading the original image, if the original image is allowed to be cached (generally, the original image will not be cached), the original image will be cached first, and then the processed image will be cached after processing.

Here, the implementation principle of hard disk cache is analyzed.

Keywords: Android architecture Cache source code analysis

Added by davidprogramer on Sun, 27 Feb 2022 23:38:07 +0200