banner
jzman

jzman

Coding、思考、自觉。
github

Bitmap memory cache and disk cache

The use of caching in Android is quite common. By employing appropriate caching strategies, it can reduce data consumption and improve application performance to some extent. For instance, when loading network images, it is not advisable to load images from the network every time; instead, they should be cached in memory and on disk, allowing retrieval from memory or disk on subsequent requests. The caching strategy typically uses the LRU (Least Recently Used) algorithm. Below, we will introduce how to use caching in Android, taking images as an example, focusing on memory caching and disk caching. The content of this article is as follows:

  1. Memory Caching
  2. Disk Caching

Memory Caching#

LruCache is a caching class provided by Android 3.1, which allows for quick access to cached Bitmap objects. Internally, it uses a LinkedHashMap to store Bitmap objects that need to be cached with strong references, releasing memory occupied by the least recently used objects when the cache exceeds a specified size.

Note: Before Android 3.1, a common memory cache was a bitmap cache using SoftReference or WeakReference, which is no longer recommended. After Android 3.1, the garbage collector pays more attention to reclaiming SoftReference/WeakReference, making this caching method largely ineffective. Using LruCache from the support-v4 compatibility package can ensure compatibility with versions prior to Android 3.1.

Using LruCache#

  1. Initialize LruCache

First, calculate the required cache size as follows:

// First method:
ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
// Get the approximate memory size occupied by the application under current hardware conditions, in MB
int memorySize = manager.getMemoryClass(); // MB
int cacheSize = memorySize / 8;
// Second method (more commonly used)
int memorySize = (int) Runtime.getRuntime().maxMemory(); // bytes
int cacheSize = memorySize / 8;

Then, initialize LruCache as follows:

// Initialize LruCache and set the cache size
LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap value) {
        // Calculate the memory size occupied by each cached Bitmap, memory units should be consistent with cacheSize
        return value.getByteCount();
    }
};
  1. Add Bitmap objects to LruCache
// Parameters put(String key, Bitmap bitmap)
lruCache.put(key, bitmap);
  1. Retrieve images from the cache and display them
// Parameters get(String key)
Bitmap bitmap = lruCache.get(key);
imageView.setImageBitmap(bitmap);

Below is a simple demonstration of using LruCache to load a network image.

Loading Network Images#

Create a simple ImageLoader that encapsulates methods for obtaining cached Bitmaps, adding Bitmaps to the cache, and removing Bitmaps from the cache, as follows:

// ImageLoader
public class ImageLoader {
    private LruCache<String, Bitmap> lruCache;
    public ImageLoader() {
        int memorySize = (int) Runtime.getRuntime().maxMemory() / 1024;

        int cacheSize = memorySize / 8;
        lruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // Calculate the memory size occupied by each cached Bitmap
                return value.getByteCount() / 1024;
            }
        };
    }

    /**
     * Add Bitmap to LruCache
     * @param key
     * @param bitmap
     */
    public void addBitmapToLruCache(String key, Bitmap bitmap) {
        if (getBitmapFromLruCache(key) == null) {
            lruCache.put(key, bitmap);
        }
    }

    /**
     * Get cached Bitmap
     * @param key
     */
    public Bitmap getBitmapFromLruCache(String key) {
        if (key != null) {
            return lruCache.get(key);
        }
        return null;
    }

    /**
     * Remove from cache
     * @param key
     */
    public void removeBitmapFromLruCache(String key) {
        if (key != null) {
            lruCache.remove(key);
        }
    }
}

Then create a thread class to load images, as follows:

// Thread for loading images
public class LoadImageThread extends Thread {
    private Activity mActivity;
    private String mImageUrl;
    private ImageLoader mImageLoader;
    private ImageView mImageView;

    public LoadImageThread(Activity activity, ImageLoader imageLoader, ImageView imageView, String imageUrl) {
        this.mActivity = activity;
        this.mImageLoader = imageLoader;
        this.mImageView = imageView;
        this.mImageUrl = imageUrl;
    }

    @Override
    public void run() {
        HttpURLConnection connection = null;
        InputStream is = null;
        try {
            URL url = new URL(mImageUrl);
            connection = (HttpURLConnection) url.openConnection();
            is = connection.getInputStream();
            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                final Bitmap bitmap = BitmapFactory.decodeStream(is);
                mImageLoader.addBitmapToLruCache("bitmap", bitmap);
                mActivity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mImageView.setImageBitmap(bitmap);
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Then, in MainActivity, use ImageLoader to load and cache network images in memory. First, retrieve from memory; if the required Bitmap is not in the cache, obtain the image from the network and add it to the cache. Once the application exits, the system will release memory. The key methods are as follows:

// Get image
private void loadImage() {
    Bitmap bitmap = imageLoader.getBitmapFromLruCache("bitmap");
    if (bitmap == null) {
        Log.i(TAG, "Getting image from the network");
        new LoadImageThread(this, imageLoader, imageView, url).start();
    } else {
        Log.i(TAG, "Getting image from cache");
        imageView.setImageBitmap(bitmap);
    }
}

// Remove from cache
private void removeBitmapFromL(String key) {
    imageLoader.removeBitmapFromLruCache(key);
}

Then call the above methods to get images and remove from cache in the corresponding events, as follows:

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.btnLoadLruCache:
            loadImage();
            break;
        case R.id.btnRemoveBitmapL:
            removeBitmapFromL("bitmap");
            break;
    }
}

Below is a log screenshot showing the execution status:

image

Disk Caching#

Disk caching refers to writing cached objects to the file system. Using disk caching can help shorten loading times when memory caching is unavailable. Retrieving images from disk cache is slower than from memory cache, so it should ideally be handled in a background thread. Disk caching uses a DiskLruCache class to implement disk caching, which is officially recommended by Google. DiskLruCache is not part of the Android SDK. Here is a link to the source code for DiskLruCache:
DiskLruCache Source Code.

Creating DiskLruCache#

The constructor for DiskLruCache is private, so it cannot be used to create a DiskLruCache instance. It provides an open method to create itself, as follows:

/**
 * Returns the cache in the specified directory; creates it if it does not exist
 * @param directory Cache directory
 * @param appVersion Indicates the application version number, generally set to 1
 * @param valueCount The number of values corresponding to each key, generally set to 1
 * @param maxSize Cache size
 * @throws IOException if reading or writing the cache directory fails
 */
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
        throws IOException {
    ...
    // Create DiskLruCache
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {
        ...
        return cache;
    }
    // If the cache directory does not exist, create the cache directory and DiskLruCache
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    ...
    return cache;
}

Note: The cache directory can be chosen as the cache directory on the SD card, i.e., /sdcard/Android/data/application package name/cache directory, or the cache directory under the current application data. Other directories can also be specified. If you want to delete cache files after the application is uninstalled, choose the cache directory on the SD card. If you want to retain data, choose another directory. Additionally, if it is a memory cache, the cache will be cleared after exiting the application.

Adding to DiskLruCache#

Adding to DiskLruCache is done through an Editor, which represents an editing object for a cached object. You can obtain the corresponding Editor object through its edit(String key) method. If the Editor is currently using edit(String key), it will return null, meaning DiskLruCache does not allow simultaneous operations on the same cached object. Of course, adding to the cache is done through a unique key. As for what to use as the key, it is generally convenient to use the MD5 value of the URL as the key, calculated as follows:

// Calculate the MD5 value of the URL as the key
private String hashKeyForDisk(String url) {
    String cacheKey;
    try {
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(url.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(url.hashCode());
    }
    return cacheKey;
}

private String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(0xFF & bytes[i]);
        if (hex.length() == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}

Once the MD5 value of the URL is obtained as the key, you can use the edit(String key) method of the DiskLruCache object to obtain the Editor object, and then use the commit method of the Editor object to release the Editor object, allowing other operations to be performed using the key.

Of course, after obtaining the key, you can add items to DiskLruCache. To load a network image into the cache, it is typically done by downloading the item and writing it to the file system. This requires an output stream to write the data. There are mainly two ways to handle this:

  1. Create an OutputStream to write the data to be cached, obtain the Editor object through DiskLruCache's edit(String key) method, then convert the OutputStream to Bitmap, write the Bitmap to the OutputStream created by the Editor object, and finally call the commit method of the Editor object to submit.
  2. First obtain the Editor object, create an OutputStream based on the Editor object, and directly write the data to be cached, then call the commit method of the Editor object to submit.

Here, we will use the first method as an example to add a network image to both the disk cache and memory cache based on the URL, as follows:

// Add network image to memory cache and disk cache
public void putCache(final String url, final CallBack callBack) {
    Log.i(TAG, "putCache...");
    new AsyncTask<String, Void, Bitmap>() {
        @Override
        protected Bitmap doInBackground(String... params) {
            String key = hashKeyForDisk(params[0]);
            DiskLruCache.Editor editor = null;
            Bitmap bitmap = null;
            try {
                URL url = new URL(params[0]);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setReadTimeout(1000 * 30);
                conn.setConnectTimeout(1000 * 30);
                ByteArrayOutputStream baos = null;
                if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
                    BufferedInputStream bis = new BufferedInputStream(conn.getInputStream());
                    baos = new ByteArrayOutputStream();
                    byte[] bytes = new byte[1024];
                    int len = -1;
                    while ((len = bis.read(bytes)) != -1) {
                        baos.write(bytes, 0, len);
                    }
                    bis.close();
                    baos.close();
                    conn.disconnect();
                }
                if (baos != null) {
                    bitmap = decodeSampledBitmapFromStream(baos.toByteArray(), 300, 200);
                    addBitmapToCache(params[0], bitmap); // Add to memory cache
                    editor = diskLruCache.edit(key);
                    // Key point
                    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, editor.newOutputStream(0));
                    editor.commit(); // Submit
                }
            } catch (IOException e) {
                try {
                    editor.abort(); // Abandon writing
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            callBack.response(bitmap);
        }
    }.execute(url);
}

Retrieving from DiskLruCache#

In the process of adding to DiskLruCache, we learned how to obtain the key. After obtaining the key, you can use the get method of the DiskLruCache object to obtain a Snapshot object, then get an InputStream from the Snapshot object, and finally obtain the Bitmap from the InputStream. You can also adjust the Bitmap sampling method as discussed in the previous article or compress it before caching. The method to obtain the InputStream is as follows:

// Get disk cache
public InputStream getDiskCache(String url) {
    Log.i(TAG, "getDiskCache...");
    String key = hashKeyForDisk(url);
    try {
        DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
        if (snapshot != null) {
            return snapshot.getInputStream(0);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

The main parts of DiskLruCache are roughly as described above. Below is an implementation of a simple three-level cache to illustrate the specific use of LruCache and DiskLruCache. The MainActivity code is as follows:

// MainActivity.java
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "cache_test";
    public static String CACHE_DIR = "diskCache";  // Cache directory
    public static int CACHE_SIZE = 1024 * 1024 * 10; // Cache size
    private ImageView imageView;
    private LruCache<String, String> lruCache;
    private LruCacheUtils cacheUtils;
    private String url = "http://img06.tooopen.com/images/20161012/tooopen_sy_181713275376.jpg";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imageView = (ImageView) findViewById(R.id.imageView);
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        cacheUtils = LruCacheUtils.getInstance();
        // Create memory cache and disk cache
        cacheUtils.createCache(this, CACHE_DIR, CACHE_SIZE);
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        cacheUtils.flush();
    }

    @Override
    protected void onStop() {
        super.onStop();
        cacheUtils.close();
    }
    
    public void loadImage(View view) {
        load(url, imageView);
    }
    
    public void removeLruCache(View view) {
        Log.i(TAG, "Removing from memory cache...");
        cacheUtils.removeLruCache(url);
    }
    
    public void removeDiskLruCache(View view) {
        Log.i(TAG, "Removing from disk cache...");
        cacheUtils.removeDiskLruCache(url);
    }
    
    private void load(String url, final ImageView imageView) {
        // Retrieve image from memory
        Bitmap bitmap = cacheUtils.getBitmapFromCache(url);
        if (bitmap == null) {
            // Retrieve image from disk
            InputStream is = cacheUtils.getDiskCache(url);
            if (is == null) {
                // Retrieve image from network
                cacheUtils.putCache(url, new LruCacheUtils.CallBack<Bitmap>() {
                    @Override
                    public void response(Bitmap bitmap1) {
                        Log.i(TAG, "Getting image from the network...");
                        Log.i(TAG, "Downloading image from the network...");
                        imageView.setImageBitmap(bitmap1);
                        Log.i(TAG, "Successfully obtained image from the network...");
                    }
                });
            } else {
                Log.i(TAG, "Getting image from disk...");
                bitmap = BitmapFactory.decodeStream(is);
                imageView.setImageBitmap(bitmap);
            }
        } else {
            Log.i(TAG, "Getting image from memory...");
            imageView.setImageBitmap(bitmap);
        }
    }
}

The layout file is relatively simple, so the code is not included. Below is a log screenshot showing the execution status:

image

This article documents the basic usage of LruCache and DiskLruCache, providing a certain understanding of these two caching helper classes. For specific implementations, please refer to the source code.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.