Android Development - Detailed Explanation of DiskLruCache Usage and In-depth Analysis of Working Principle

Overview
LruCache solves the problem of memory leaks when loading large images and multiple images, but another problem follows: after the image is removed from memory, when the user wants to reload the removed image, it can only be downloaded from the network. Reloading once, obviously very time-consuming, is the most commonly used function of photo wall applications. Google also provides a set of hard disk caching solutions: DiskLruCache (not officially written by Google, but officially certified). Unfortunately, the Android Doc does not give a detailed description of the usage of DiskLruCache, and there is very little information about DiskLruCache on the Internet, so today I am going to write a special blog to explain the usage of DiskLruCache in detail and analyze it. Working principle, this should also be the most detailed information about DiskLruCache on the Internet.

    So let's take a look at which applications have used DiskLruCache technology. In the range of applications I have been in contact with, Dropbox, Twitter, NetEase News, etc. all use DiskLruCache for hard disk caching. Most of Dropbox and Twitter should have never used it, so let's start with the most familiar NetEase News. Start the analysis and have an initial understanding of DiskLruCache.      I believe everyone knows that the data in NetEase News is obtained from the Internet, including a lot of news content and news pictures, as shown in the following figure
     :

     But I don't know if you have found that these contents and pictures will be stored in the local cache after they are obtained from the Internet, so even if the mobile phone is not connected to the Internet, it can still load the news that has been browsed before. Needless to say, the caching technology used is naturally DiskLruCache, so the first question is, where are these data cached in the mobile phone? In fact, DiskLruCache does not limit the data cache location and can be set freely, but usually most applications will choose the cache location as the path /sdcard/Android/data/<application package>/cache. There are two advantages to choosing this location: first, this is stored on the SD card, so even more data cached will not have any effect on the phone's built-in storage space, as long as the SD card space is sufficient. Second, this path is recognized by the Android system as the cache path of the application. When the program is uninstalled, the data here will also be cleared together, so that there will be no problem of a lot of residual data on the phone after the program is deleted. . So here is Netease News as an example. The package name of its client is com.netease.newsreader.activity, so the data cache address should be /sdcard/Android/data/com.netease.newsreader.activity/cache, we enter Take a look in this directory, and the result is shown in the following figure:

     You can see that there are many folders, because NetEase News caches various types of data. For simplicity, we only analyze the image cache, so enter the in the bitmap folder. Then you will see a bunch of files with very long file names, these files are named without any rules, and you can't understand what they mean, but if you scroll all the way down, you will see a file called journal, as follows As shown in the figure:
     So what exactly are these files? Seeing this, I believe that some friends are already confused, here I will briefly explain. The files with long file names above are cached pictures, each file corresponds to a picture, and the journal file is a log file of DiskLruCache, and the operation records of the program for each picture are stored in this file. , basically seeing the journal file indicates that the program uses DiskLruCache technology.
    After downloading
     , after having an initial understanding of DiskLruCache, let's learn how to use DiskLruCache. Since DiskLruCache is not officially written by Google, this class is not included in the Android API. We need to download this class from the Internet and add it to the project manually. The source code of DiskLruCache is hosted on Github at the following address:
https://github.com/JakeWharton/DiskLruCache After downloading the source code, you only need to create a new libcore.io package in the project, and then copy the DiskLruCache.java file to this package in.
     If the cache is turned
     on, we have done the preparation work. Let's take a look at how DiskLruCache is used. First of all, you need to know that DiskLruCache cannot create new instances. If we want to create an instance of DiskLruCache, we need to call its open() method. The interface is as follows:
1
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) ;
The open() method receives four parameters. The first parameter specifies the cache address of the data, the second parameter specifies the version number of the current application, and the third parameter specifies how many cache files the same key can correspond to. Is to pass 1, the fourth parameter specifies how many bytes of data can be cached at most.
     The cache address has been mentioned before, and it is usually stored under the path /sdcard/Android/data/<application package>/cache, but at the same time we need to consider if the phone does not have an SD card, or the SD is just removed. Therefore, better programs will write a method to get the cache address, as shown below:
1
public File getDiskCacheDir(Context context, String uniqueName) {
2
    String cachePath;
3
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState ())
4
            || !Environment.isExternalStorageRemovable()) {
5
        cachePath = context.getExternalCacheDir().getPath();
6
    } else {
7
        cachePath = context.getCacheDir().getPath();
8
    }
9
    return new File(cachePath + File.separator + uniqueName);
10
}
     You can see that when the SD card exists or the SD card cannot be removed, call the getExternalCacheDir() method to get the cache path, otherwise call getCacheDir( ) method to get the cache path. The former gets the path /sdcard/Android/data/<application package>/cache, while the latter gets the path /data/data/<application package>/cache.
Then, the obtained path is spliced ​​with a uniqueName and returned as the final cache path. So what is this uniqueName? In fact, this is a unique value set to distinguish different types of data, such as the bitmap, object and other folders seen under the NetEase News cache path. Next is the application version number, we can simply get the version number of the current application using the following code:
1
public int getAppVersion(Context context) {
2
    try {
3
        PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName (), 0);
4
        return info.versionCode;
5
    } catch (NameNotFoundException e) {
6
        e.printStackTrace();
7
    }
8
    return 1;
9
}
     It should be noted that whenever the version number changes, all data stored in the cache path will be cleared, because DiskLruCache thinks that when the application When the program has a version update, all data should be retrieved from the Internet.
The last two parameters have nothing to explain. The third parameter is passed 1, and the fourth parameter is usually passed in the size of 10M, which can be adjusted according to its own situation. So a pretty standard open() method would be:
1
DiskLruCache mDiskLruCache = null;
2
try {
3
    File cacheDir = getDiskCacheDir(context, "bitmap");
4
    if (!cacheDir.exists()) {
5
        cacheDir .mkdirs();
6
    }
7
    mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
8
} catch (IOException e) {
9
    e.printStackTrace();
10
}
     First call the getDiskCacheDir() method to get the path to the cache address, and then judge whether the path exists, and create it if it does not exist. Then call the open() method of DiskLruCache to create an instance, and pass in the four parameters.
With the instance of DiskLruCache, we can operate the cached data. The operation types mainly include writing, accessing, removing, etc. We will learn one by one.
     To write to the cache
     , let’s look at writing first. For example, there is a picture at http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg, then in order to download this picture, just It can be written like this:
1
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
2
    HttpURLConnection urlConnection = null;
3
    BufferedOutputStream out = null;
4
    BufferedInputStream in = null;
5
    try {
6
        final URL url = new URL(urlString);
7
        urlConnection = (HttpURLConnection) url.openConnection();
8
        in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
9
        out = new BufferedOutputStream(outputStream, 8 * 1024);
10
        int b;
11
        while ((b = in.read()) != -1) {
12
            out.write(b);
13
        }
14
        return true;
15
    } catch (final IOException e) {
16
        e.printStackTrace();
17
    } finally {
18
        if (urlConnection != null) {
19
            urlConnection.disconnect();
20
        }
21
        try {
22
            if (out != null) {
23
                out.close();
24
            }
25
            if (in != null) {
26
                in.close();
27
            }
28
        } catch (final IOException e) {
29
            e.printStackTrace();
30
        }
31
    }
32
    return false;
33
}
     This code is quite basic. I believe everyone can understand it. It is to access the URL passed in urlString and write it locally through outputStream. With this method, we can use DiskLruCache to write, and the writing operation is done with the help of the DiskLruCache.Editor class. Similarly, this class cannot be new either. You need to call the edit() method of DiskLruCache to get the instance. The interface is as follows:
1
public Editor edit(String key) throws IOException As
you can see, the edit() method receives a parameter key, This key will be the filename of the cached file and must correspond to the URL of the image. So how can we make the key and the URL of the image correspond one-to-one? Use the URL directly as the key? Not very suitable, because the image URL may contain some special characters that may not be legal when naming the file. In fact, the easiest way is to MD5 encode the URL of the picture. The encoded string must be unique and only contain characters such as 0-F, which fully conforms to the naming rules of the file.
Then we write a method to MD5 encode the string, the code is as follows:

1
public String hashKeyForDisk(String key) {
2
    String cacheKey;
3
    try {
4
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");
5
        mDigest.update(key.getBytes());
6
        cacheKey = bytesToHexString(mDigest.digest());
7
    } catch (NoSuchAlgorithmException e) {
8
        cacheKey = String.valueOf(key.hashCode());
9
    }
10
    return cacheKey;
11
}
12

13
private String bytesToHexString(byte[] bytes) {
14
    StringBuilder sb = new StringBuilder();
15
    for (int i = 0; i < bytes.length; i++) {
16
        String hex = Integer.toHexString(0xFF & bytes[i]);
17
        if (hex.length() == 1) {
18
            sb.append('0');
19
        }
20
        sb.append(hex);
21
    }
22
    return sb.toString();
23
}
The code is very simple, now we just need to call the hashKeyForDisk() method and pass the URL of the image into this method, then Get the corresponding key.
So, now you can get an instance of DiskLruCache.Editor by writing:

1
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
2
String key = hashKeyForDisk( imageUrl);
3
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
After having an instance of DiskLruCache.Editor, we can call its newOutputStream() method to create an output stream, and then pass it into downloadUrlToStream() The function of downloading and writing to the cache can be realized. Note that the newOutputStream() method receives an index parameter. Since 1 was specified when setting valueCount, the index can be passed as 0 here. After the write operation is completed, we also need to call the commit() method to commit to make the write take effect, and calling the abort() method means to abandon the write.
So the code for a complete write operation looks like this:

1
new Thread(new Runnable() {
2
    @Override
3
    public void run() {
4
        try {
5
            String imageUrl = "http://img.my.csdn. net/uploads/201309/01/1378037235_7476.jpg";
6
            String key = hashKeyForDisk(imageUrl);
7
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);
8
            if (editor != null) {
9
                OutputStream outputStream = editor. newOutputStream(0);
10
                if (downloadUrlToStream(imageUrl, outputStream)) {
11
                    editor.commit();
12
                } else {
13
                    editor.abort();
14
                }
15
            }
16
            mDiskLruCache.flush();
17
        } catch (IOException e) {
18
            e.printStackTrace();
19
        }
20
    }
21 }) .
start();
The downloadUrlToStream() method is called to download images from the network, so make sure this code is executed in a child thread. Note that I also called the flush() method at the end of the code. This method does not have to be called every time I write, but it is indispensable here. I will explain its function later.
Now, the cache should have been successfully written. Let's go to the cache directory on the SD card and take a look, as shown in the following figure: As

     you can see, there is a file with a long file name, and a journal file, that file A file with a long name is naturally a cached image, because it is named using MD5 encoding.

read cache
     After the cache has been written successfully, it is time to learn how to read it. The method of reading is simpler than writing. It is mainly implemented with the get() method of DiskLruCache. The interface is as follows:

1
public synchronized Snapshot get(String key) throws IOException
Obviously, the get() method requires a key to get the corresponding cached data, and this key is undoubtedly the value after MD5 encoding the image URL, so the code to read the cached data can be written like this:

1
String imageUrl = "http://img. my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
2
String key = hashKeyForDisk(imageUrl);
3
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
     Strangely, what is obtained here is a DiskLruCache.Snapshot object, how should we use this object? Very simple, just call its getInputStream() method to get the input stream of the cached file. Similarly, the getInputStream() method also needs to pass an index parameter, and it is good to pass 0 here. With the input stream of the file, it is easy to display the cached image on the interface. So, a complete code to read the cache and load the image to the interface is as follows:
1
try {
2
    String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
3
    String key = hashKeyForDisk(imageUrl);
4
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
5
    if (snapShot != null) {
6
        InputStream is = snapShot.getInputStream(0);
7
        Bitmap bitmap = BitmapFactory.decodeStream(is);
8
        mImage.setImageBitmap(bitmap);
9
    }
10
} catch (IOException e) {
11
    e .printStackTrace();
12
}
     We used the decodeStream() method of BitmapFactory to parse the file stream into a Bitmap object, and then set it to the ImageView. If you run the program, you will see the following effect:
OK, the picture has been successfully displayed. Note that we are loading from the local cache, not from the network, so this image will still be displayed even when your phone is not connected to the Internet.
     Remove the cache
     After learning the methods of writing to the cache and reading the cache, you have mastered the two most difficult operations, so the next step to learn to remove the cache must be very easy for you. Removing the cache is mainly implemented with the remove() method of DiskLruCache. The interface is as follows:
1
public synchronized boolean remove(String key) throws IOException
I believe you are quite familiar with it. The remove() method requires a key to be passed in, and then The cached image corresponding to this key will be deleted. The sample code is as follows:

1
try {
2
    String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
3
    String key = hashKeyForDisk( imageUrl);  
4
    mDiskLruCache.remove(key);
5
} catch (IOException e) {
6
    e.printStackTrace();
7
}
     The usage is simple, but you need to know that this method should not be called often. Because you don't need to worry about too much cached data taking up too much space on the SD card, DiskLruCache will automatically delete the excess cache according to the cache maximum value we set when calling the open() method. You should call the remove() method to remove the cache only when you are sure that the cache content corresponding to a key has expired and you need to get the latest data from the network.
    
     Other APIs
     In addition to writing cache, reading cache, and removing cache, DiskLruCache also provides some other commonly used APIs, which we will briefly learn. 1. size() This method will return the total number of bytes of all cached data in the current cache path, in bytes. If the application needs to display the total size of the current cached data on the interface, it can be calculated by calling this method. come out. For example, there is such a function in NetEase News, as shown in the following figure:
2.flush() This method is used to synchronize the operation records in the memory to the log file (that is, the journal file). This method is very important, because the premise that DiskLruCache can work properly depends on the content of the journal file. I called this method once when I explained the write cache operation, but in fact, it is not necessary to call the flush() method every time the cache is written. Frequent calls will not bring any benefits, only extra Increase the time to synchronize journal files. The standard approach is to call the flush() method once in the onPause() method of the Activity. 3.close() This method is used to close DiskLruCache, which is a method corresponding to the open() method. After it is closed, you can no longer call any method in DiskLruCache that manipulates the cached data. Usually, you should only call the close() method in the onDestroy() method of the Activity.
4. delete()

This method is used to delete all cached data. For example, the manual cache cleaning function in NetEase News, in fact, only needs to call the delete() method of DiskLruCache to achieve it.

Interpreting the journal
As mentioned earlier, the premise of DiskLruCache's normal operation depends on the content in the journal file. Therefore, being able to read the journal file is very important for us to understand the working principle of DiskLruCache. So what exactly does the content of the journal file look like? Let's open it up and take a look, as shown in the following figure:


Since only one image is cached now, there are not a few lines of logs in the journal, so we analyze it line by line. The first line is a fixed string "libcore.io.DiskLruCache", indicating that we are using DiskLruCache technology. The second line is the version number of DiskLruCache, which is always 1. The third line is the version number of the application. What is the version number we passed in in the open() method will be displayed here. The fourth line is valueCount, which is also passed in the open() method, usually 1. The fifth line is a blank line. The first five lines are also known as the header of the journal file. This part of the content is relatively easy to understand, but the next part will require a little brainstorming.

The sixth line starts with a DIRTY prefix, followed by the cached image key. Usually we see the word DIRTY does not mean anything good, it means that this is a dirty data. That's right, every time we call the edit() method of DiskLruCache, a DIRTY record will be written to the journal file, indicating that we are preparing to write a cached data, but I don't know what the result will be. Then call the commit() method to indicate that the write to the cache is successful. At this time, a CLEAN record will be written to the journal, which means that the "dirty" data has been "cleaned", and the abort() method is called to indicate that the write to the cache failed. At this time, a REMOVE record will be written to the journal. That is to say, each line of DIRTY key should be followed by a corresponding CLEAN or REMOVE record, otherwise the data is "dirty" and will be automatically deleted.

If you are careful enough, you should also notice that the record on the seventh line, in addition to the CLEAN prefix and key, is followed by a 152313. What does this mean? In fact, DiskLruCache will add the size of the cached data at the end of each line of CLEAN records, in bytes. 152313 is the number of bytes of the image we cached, which is about 148.74K, which is just as big as the cached image, as shown in the following figure:


The size() method we learned earlier can get the current cache path The total number of bytes of all cached data below, in fact, its working principle is to add the number of bytes of all CLEAN records in the journal file, and then return it to the total.

In addition to DIRTY, CLEAN, REMOVE, there is also a record prefixed with READ. This is very simple. Whenever we call the get() method to read a piece of cached data, a READ will be written to the journal file. Record. Therefore, a program like NetEase News with a large amount of pictures and data may have a large number of READ records in the journal file.

Then you may be worried, if I keep operating frequently, I will keep writing data to the journal file, so will the journal file get bigger and bigger? Don't worry, DiskLruCache uses a redundantOpCount variable to record the number of user operations. Every time a write, read or remove cache operation is performed, the value of this variable will increase by 1. When the variable value reaches 2000, it will be Trigger the event of journal reconstruction. At this time, some redundant and unnecessary records in the journal will be automatically removed to ensure that the size of the journal file is always kept within a reasonable range.

Well, in this case, we have finished analyzing the usage and brief working principle of DiskLruCache. As for the source code of DiskLruCache, it is relatively simple. Due to space limitations, it will not be expanded here. Interested friends can explore it by themselves. In the next article, I will take you through a project to understand the usage of DiskLruCache more deeply. Interested friends, please continue to read the full version of Android Photo Wall, which perfectly combines LruCache and DiskLruCache.

 

Reprinted from  http://www.mobile-open.com/2014/3104.html

 

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=327059523&siteId=291194637