Glide picture disk cache

In the article “Glide to achieve the cool display effect of WebView offline pictures”, we use Glide to cache the pictures in WebView, and save the HTML content to the file, and finally can realize the article reading offline. Glide, which is used to load network images into memory and disk. The next time we use it, we can get the display image from the cache. The disk cache uses the DiskLruCache algorithm. However, the total size of the cache file is limited. When it exceeds 250M(the default), some unused image resources will be deleted according to the LRU algorithm. But this will cause some WebView images to fail to load offline (if). So what we want is that if the HTML page is saved offline, then the images in it will also be put into a permanent directory, and these image resources will not be recorded in the LRU.

Glide disk cache process source analysis

In order to achieve the above image permanence effect, a custom disk caching policy is required. Before we do that, we need to take a look at Glide’s source code, how it caches images to disk, and how it uses the image resources on disk.

Glide.with(this).load(url).into(image)
Copy the code

Debugging through ASintoMethod, which is eventually calledRequestManagerthetrackMethod,requestThe actual type isSingleRequest:

Enter thetrackMethod is found to executeSingleRequestthebeginmethods

debugSingleRequestthebeginMethod, executedDrawableImageViewTargetthegetSizeMethod, and passes itself as a callback argument of typeSizeReadyCallback, guess estimate final callonSizeReadyMethods.

Search by AS shortcut (MAC uses Command + O)DrawableImageViewTargetClass, and then searchgetSizeMethod (Command +F12, type getSize) to find the corresponding implementation classViewTarget.

Continue to debugViewTargetThe method can be found throughViewTreeObserverTo obtainImageViewGet the size, and then finally call backSingleRequesttheonSizeReadyMethods.

Go back toSingleRequesttheonSizeReadyMethods.statusSet toRUNNINGState. Then performEngineIn theThe load method, followed up and found that it was executedDecodeJobtherunWrappedMethods.

In runGenerators method, cycle performed currentGenerator. StartNext () method. There are three implementation classes for the DataFetcherGenerator interface

  • ResourceCacheGenerator
  • DataCacheGenerator
  • SourceGenerator

And in theSourceGeneratorthestartNextWill performcacheToDataCache disk operation, passDiskLruCacheWrappertheputMethod to cache image resources to disk.

The final disk caching algorithm is implemented by DiskLruCache class.

DiskLruCache

Glide uses DiskLruCache to cache the image’s original resources to disk, along with some resources of specified execution size. It contains an index file named Journal and multiple hashed image resource files. A journal file might look like this:

libcore.io.DiskLruCache
1
100
2

CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
Copy the code

The first four lines indicate the file header information, the disk cache version, the application version (), and the number of cached entries (valueCount). Line 6 begins with the cache records in different states, separated by Spaces:

State + Key + Specifies attributes related to the stateCopy the code
  • DIRTYIndicates that data is being created or updated, eachDIRTYThere should always be one after dirty operationsCLEANorREMOVEOperation, no matchCLEANorREMOVEDirty lines indicate that temporary files may need to be deleted.
  • CLEANRepresents the cache entry that has been successfully published and may be read, followed by the length of each value, ifvalueCountIs 2, there are two fileskey.0andkey.1. The next two values indicate the file length.
  • READRepresents the read record of the image, which goes into the LRU.
  • REMOVERepresents the deletion of cached resources.

The default maximum size of the DiskLruCache cache is 250 MB. When the total size of the image file operation is greater than this value, the REMOVE operation is performed.

//DiskLruCache.java
private void trimToSize(a) throws IOException {
	while(size > maxSize) { Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); remove(toEvict.getKey()); }}Copy the code

If the images we cache offline need to be used permanently, we can’t count those offline images in LRU, otherwise some of the offline images will be deleted when the cache file exceeds 250 MB one day. So we need to define a new PERMANENT state that doesn’t enter LRU, and we put these images in a different folder.

Custom DiskLruCache supports permanent storage

First we need to enable Glide to customize DiskCache, define an AppGlideModule using @glidemodule, and implement cache customization in the applyOptions method.

@GlideModule
public class MyAppGlideModule extends AppGlideModule {
    
    @Override
    public void applyOptions(@NonNull Context context,
                             @NonNull GlideBuilder builder) {
        super.applyOptions(context, builder);
        builder.setDiskCache(new WanDiskCacheFactory(new WanDiskCacheFactory.CacheDirectoryGetter() {
            @NotNull
            @Override
            public File getCacheDirectory(a) {
                return new File(context.getCacheDir(), "wandroid_images");
            }

            @NotNull
            @Override
            public File getPermanentDirectory(a) {
                return new File(context.getFilesDir(), "permanent_images"); }})); }}Copy the code

Modify DiskCacheFactory source to WanDiskCacheFactory, add permanentDirectory

class WanDiskCacheFactory(var cacheDirectoryGetter: CacheDirectoryGetter) :
    DiskCache.Factory {

    interface CacheDirectoryGetter {
        val cacheDirectory: File
        val permanentDirectory: File
    }

    override fun build(a): DiskCache? {
        val cacheDir: File =
            cacheDirectoryGetter.cacheDirectory
        val permanentDirectory = cacheDirectoryGetter.permanentDirectory
        cacheDir.mkdirs()
        permanentDirectory.mkdirs()

        return if((! cacheDir.exists() || ! cacheDir.isDirectory || ! permanentDirectory.exists() || ! permanentDirectory.isDirectory) ) {null
        } else WanDiskLruCacheWrapper.create(
            permanentDirectory,
            cacheDir,
            20 * 1024 * 1024//262144000L(250M) for cache)}}Copy the code

Change the value of “DiskLruCache” to “WanDiskLruCache” and add “PERMANENT” to support PERMANENT image storage.

public final class WanDiskLruCache implements Closeable {
	static final String MAGIC = "libcore.io.WanDiskLruCache"; .private static final String CLEAN = "CLEAN";
    private static final String DIRTY = "DIRTY";
    private static final String REMOVE = "REMOVE";
    private static final String READ = "READ";
	private static final String PERMANENT = "PERMANENT";// Long file (not in LRU)
	private void readJournalLine(String line) throws IOException {
        int firstSpace = line.indexOf(' ');
        if (firstSpace == -1) {
            throw new IOException("unexpected journal line: " + line);
        }

        int keyBegin = firstSpace + 1;
        int secondSpace = line.indexOf(' ', keyBegin);
        final String key;
        if (secondSpace == -1) {
            key = line.substring(keyBegin);
            if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
                lruEntries.remove(key);
                permanentEntries.remove(key);
                return;
            }
            / / permanent area
            if (firstSpace == PERMANENT.length() && line.startsWith(PERMANENT)) {
                lruEntries.remove(key);
                Entry entry = permanentEntries.get(key);
                if (entry == null) {
                    entry = new Entry(key, permanentDirectory);
                    entry.readable = true;
                    permanentEntries.put(key, entry);
                }
                return; }}else{ key = line.substring(keyBegin, secondSpace); }... }public synchronized Value get(String key) throws IOException {
        checkNotClosed();
        Entry permanentEntry = readPermanentEntry(key);
        if(permanentEntry ! =null) {
            StringKt.logV("read from permanent permanent directory:" + key);
            return newValue(key, permanentEntry.sequenceNumber, permanentEntry.cleanFiles, permanentEntry.lengths); }... }/** * Reads files from the permanent area **@param key
     * @return* /
    private Entry readPermanentEntry(String key) throws IOException {
        Entry entry = permanentEntries.get(key);
        if (entry == null) {
            entry = new Entry(key, permanentDirectory);
            entry.readable = true;
            for (File file : entry.cleanFiles) {
                // A file must have been deleted manually!
                if(! file.exists()) {return null;
                }
            }
            addOpt(PERMANENT, key);
        }
        return entry;
    }
    /** * Move the cached file to permanent */
    public synchronized boolean cacheToPermanent(String key) throws IOException {
        checkNotClosed();
        Entry entry = lruEntries.get(key);
        if (entry == null|| entry.currentEditor ! =null) {
            StringKt.logV("cacheToPermanent null:" + key);
            return false;
        }

        for (int i = 0; i < valueCount; i++) {
            File file = entry.getCleanFile(i);
            if (file.exists()) {
                FileUtil.copyFileToDirectory(file, permanentDirectory);
                file.delete();
            }
            size -= entry.lengths[i];
            StringKt.logV("cacheToPermanent:" + entry.getLengths() + ",key:" + entry.key);
            entry.lengths[i] = 0;
        }
        Entry pEntry = new Entry(key, permanentDirectory);
        pEntry.readable = true;
        permanentEntries.put(key, pEntry);
        lruEntries.remove(key);
        addOpt(PERMANENT, key);

        return true;
    }

    /** * delete permanent image **@param key
     * @return
     * @throws IOException
     */
    public synchronized boolean removePermanent(String key) throws IOException {
        checkNotClosed();
        Entry entry = readPermanentEntry(key);
        if (entry == null) return false;
        for (int i = 0; i < valueCount; i++) {
            File file = entry.getCleanFile(i);
            if(file.exists() && ! file.delete()) {throw new IOException("failed to delete " + file);
            }
            permanentEntries.remove(key);
        }
        addOpt(REMOVE, key);
        return true;
    }

    /** * Place the downloaded TMP file in the permanent area */
    public synchronized boolean tempToPermanent(File tmp, String key) throws IOException {
        StringKt.logV("tempToPermanent:" + key);
        Entry entry = new Entry(key, permanentDirectory);
        for (int i = 0; i < valueCount; i++) {
            File file = entry.getCleanFile(i);
            tmp.renameTo(file);
        }
        permanentEntries.put(key, entry);
        addOpt(PERMANENT, key);
        return true; }}Copy the code

Glide to achieve WebView offline picture displayThere are two situations when the image resources are permanently stored. One is that the image has been loaded and there are related resources in the disk, and we can passcacheToPermanentMethod to move the image from the cache directory to the permanent directory. The other one is if the image is not loaded, in this case you need to download the image for offline caching and thentempToPermanentDirectly into the permanent area.

The project address

Github.com/iamyours/Wa…