The Bitmap cache pool in Android is used in detail

  • 2020-06-12 10:36:28
  • OfStack

This article showed you how to use caching to improve the smoothness of UI's load input and sliding. Using memory caching, using disk caching, handling configuration change events, and so on will effectively solve this problem.

Displaying a single image in your UI is very simple, and a little more complicated if you need to display many images at once. In many cases (using ListView, GridView, or ViewPager controls, for example), the number of images displayed on the screen, and the number of images to be displayed on the screen, is very large (for example, viewing a large number of images in a gallery).

In these controls, when a child control is not displayed, the system reuses the control to loop the display to reduce memory consumption. The garbage collection mechanism also frees Bitmap resources that have been loaded into memory (assuming you don't strongly reference them). This is generally all well and good, but to ensure UI's smoothness and load images efficiently when users swipe back and forth across the screen, you need to avoid duplicating the images that need to be displayed. Using memory cache and disk cache can solve this problem. Using cache allows the control to quickly load the processed images.

This article describes how to use caching to improve the smoothness of the UI's load input and sliding.

Use memory cache

The memory cache increases the speed of accessing images, but it takes up a lot of memory. LruCache
Classes (which can be used in Support Library before API 4) are particularly good for caching Bitmap and putting the most recently used
The Bitmap object is stored with strong references (in LinkedHashMap), and when the number of caches reaches a predetermined value, the
Removal of infrequently used objects.

Note: In the past, the common practice to implement memory caching was to use
SoftReference or
WeakReference bitmap cache,
However, this is not recommended. Starting with Android 2.3 (API Level 9), garbage collection began to enforce the recycling of soft/weak references, resulting in no efficiency gains for these cages.
In addition, prior to Android 3.0 (API Level 11), these cached Bitmap data were stored in the underlying memory (native memory) and would not be released once the predefined conditions were met, which could result in
The program exceeded the memory limit and crashed.

When using LruCache, you need to consider the following factors to select an appropriate cache quantity parameter:

1. How much memory is available in the program
2. How many pictures are displayed on the screen at the same time? How many images should I cache to display on the screen I'm about to see?
3. What is the screen size and screen density of the device? Super high screen density (xhdpi e.g. Galaxy Nexus)
4. A device displaying the same image requires more memory than a device with a lower screen density (hdpi such as Nexus S).
5. The size and format of the image determine how much memory each image needs
6. How often are images accessed? 1. Are some images accessed much more frequently than others? If so, you might want to put these frequently accessed images in memory.
7. How to balance the quality and quantity? In some cases it is useful to keep a large number of low quality images, using background threads to add a high quality version of the image when needed.

There is no one-size-fits-all formula, and you need to analyze your usage and specify your own caching strategy. Using too small a cache does not work as well, and using too large a cache consumes more
It is possible, therefore, to cause an ES71en.lang.OutOfMemory exception or to leave little memory for other functions of your program.

Here is an example of using the LruCache cache:


private LruCache<string, bitmap=""> mMemoryCache; @Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get memory class of this device, exceeding this amount will throw an
    // OutOfMemory exception.
    final int memClass = ((ActivityManager) context.getSystemService(
            Context.ACTIVITY_SERVICE)).getMemoryClass();     // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = 1024 * 1024 * memClass / 8;     mMemoryCache = new LruCache<string, bitmap="">(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in bytes rather than number of items.
            return bitmap.getByteCount();
        }
    };
    ...
}                                                              
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}                                                              
public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

Note: In this example, 1/8 of the program's memory is used for caching. This has at least 4MB(32/8) memory in one normal/hdpi device.
Almost 1.5MB(800*480*4 bytes) will be used when the full GridView is fully filled with images on a device with 800×480 resolution
So that's about 2.5 pages of images cached in memory.

When displaying images in ImageView,
Check to see if LruCache is present. If it exists, use the cached image. If it doesn't, start the background thread to load the image and cache it:


public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

BitmapWorkerTask needs to add new images to the cache:

class BitmapWorkerTask extends AsyncTask<integer, void,="" bitmap=""> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

The next page shows you two other ways to use disk caching and handle configuration change events

Use disk caching

Memory caching is fast when accessing recently used images, but you can't be sure if the image exists in the cache. like
A control like GridView can have a lot of images to display, and soon the image data fills up the cache.
Your program may also be interrupted by other tasks, such as incoming phone calls -- the system may be aware of these image caches when your program is in the background. Once the user returns to your application, you will need to reprocess the images.

In this case, the disk cache can be used to hold the images that have already been processed, and when the images are not available in the memory cache, they can be loaded from the disk cache to skip the image processing.
Of course, loading images from disk is much slower than reading them from memory, and disk images should be loaded in a non-ES110en thread.

Note: If cached images are used frequently, consider using them
ContentProvider, for example, does this in the gallery program.

There is a simple implementation of DiskLruCache in the sample code. Then, in Android 4.0 contains a more reliable and it is recommended to use DiskLruCache (libcore/luni/src/main/java/libcore/io/DiskLruCache java)
. You can easily transplant the implementation to the previous version 4.0 (http href = ": / / www google. com/search & # 63; q = disklrucache" > Google1 See if anyone else has already done this!) .

Here is an updated version of DiskLruCache:


private DiskLruCache mDiskCache;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails"; @Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Initialize memory cache
    ...
    File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
    mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
    ...
}                               
class BitmapWorkerTask extends AsyncTask<integer, void,="" bitmap=""> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);         // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);         if (bitmap == null) { // Not found in disk cache
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }                              
        // Add final bitmap to caches
        addBitmapToCache(String.valueOf(imageKey, bitmap);         return bitmap;
    }
    ...
}                               
public void addBitmapToCache(String key, Bitmap bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }                               
    // Also add to disk cache
    if (!mDiskCache.containsKey(key)) {
        mDiskCache.put(key, bitmap);
    }
}                               
public Bitmap getBitmapFromDiskCache(String key) {
    return mDiskCache.get(key);
}                               
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
            || !Environment.isExternalStorageRemovable() ?
                    context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();
    return new File(cachePath + File.separator + uniqueName);
}

The memory cache is detected in the UI thread and the disk cache is detected in the background thread. Disk operations should never be implemented in the UI thread. When the image is processed, the final result will be added to
Memory cache and disk cache for future use.

Handles configuration change events

A configuration change at runtime - such as a screen orientation change - causes Android to destroy the running Activity and then use it
The new configuration restarts the Activity (see Handling Runtime Changes for details).
You need to be careful not to reprocess all images when configuration changes occur to improve the user experience.

Fortunately, you already have a good image cache in the memory cache section. The cache is passable
Fragment (Fragment is saved via the setRetainInstance(true) function) is passed to the new Activity
When Activity is restarted, Fragment is reattached to Activity, which you can use to get cached objects.

Here is an example of saving a cache in Fragment:


private LruCache<string, bitmap=""> mMemoryCache;                 
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment mRetainFragment =            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = RetainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<string, bitmap="">(cacheSize) {
            ... // Initialize cache here as usual
        }
        mRetainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}                 
class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache<string, bitmap=""> mRetainedCache;     public RetainFragment() {}                 
    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
        }
        return fragment;
    }                 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        <strong>setRetainInstance(true);</strong>
    }
}

In addition, you can try rotating the device's screen direction with and without Fragment to see the actual image load.


Related articles: