Android memory and performance optimization. This tutorial describes how to optimize the usage of memory and optimize your performance in your Android application. The tutorial is based on Android Studio.

1. Programming tips for providing highly responsive and fast Android applications

1.1. Why you should be careful with Android resources

Android devices have less power than standard desktop or notebook computers. For this reason you must be careful with memory consumption.

Especially on Android devices before Android 5.0 you want to avoid triggering the garbage collector of the Java virtual machine. This results in a freeze of the Android runtime for about 200 ms. This can be a notable delay, if the user is, for example, scrolling down a list.

1.2. Avoid unnecessary object allocation

Avoid creating unnecessary objects, especially in expensive places. Reuse objects if possible. Creating unnecessary objects triggers the garbage collection more frequently, and this should be avoided.

For example avoid object creating in loops or in the onDraw() method of your custom view.

1.3. Use efficient data structures

Android provides several implementations of Sparse*Array classes. Consider the following code.

Map<Integer, String> map = new HashMap<Integer, String>();

Using this code results in unnecessary Integer objects created.

Android provides data structures which are more efficient for mapping values to other objects. If possible use these objects, they avoid object creation as in the case of using HashMap. Object creation can be expensive and should be avoided to reduce the number of times the garbage collector needs to run.

The table give examples for SparseArrays.

Table 1. Efficient memory structures
Memory structure Description

SparseArray<E>

Maps integers to Objects, avoid the creation of Integer objects.

SparseBooleanArray

Maps integers to booleans.

SparseIntArray

Maps integers to integers

To improve the above example, prefer to use the following data structure.

SparseArray<String> map = new SparseArray<String>();
map.put(1, "Hello");

2. Handling bitmaps

Bitmaps can allocate lots of memory if loaded at full size. It is recommended to load the bitmaps in the desired size into memory. Assume you have an application which displays an image in 100x100 dp, you should load the image in exactly this size.

A common way is to first measure the bitmap without loading it via a flag passed to the BitmapFactory.

// instruct BitmapFactory to only the bounds and type of the image
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);

// get width and height
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
// type of the image
String imageType = options.outMimeType;

Afterwards you can load the scaled version of the image. Android is really good in scaling images by a power of two. You can use the following method (from the official Android documentation) to calculate the scale factor on a basis of 2.

public static Bitmap decodeBitmapWithGiveSizeFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

This method can be used to assign the image to a view as demonstrated in the following example.

viewWidth = imageView.getWidth();
viewHeight = imageView.getHeight();

imageView.
imageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, viewWidth, viewHeight));

3. Using caches

3.1. Using a cache

A cache allows reusing objects which are expensive to create. If you load on object into memory, you can think of this as a cache for the object. For example, if you downloading images from the Internet to display them in a list you should hold them in memory to avoid that you download them several times.

At some point you need to recycle some of your objects, otherwise you run out of memory. A good approach to do this, is to recycle the objects which have not been used the longest in your application.

The Android platform provides the LruCache class, as of API 12 (or in the support-v4 library). The LruCache class provides a _least recently used cache _ (LRU cache) cache implementation. A LRU cache keeps track of the usage of its members. It has a given size and if this size is exceeded, it removes the items which have not be accessed the longest. This behavior is depicted in the following graphic.

LRU cache in general

The following example code demonstrates a possible implementation of the LruCache class for caching images.

public class ImageCache extends LruCache<String, Bitmap> {
 
  public ImageCache( int maxSize ) {
    super( maxSize );
  }
 
  @Override
  protected int sizeOf( String key, Bitmap value ) {
    return value.getByteCount();
  }
 
  @Override
  protected void entryRemoved( boolean evicted, String key, Bitmap oldValue, Bitmap newValue ) {
    oldValue.recycle();
  }
 
}

Its usage is simple and demonstrated by the following example code.

LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>();

For determining the initial size of the cache, it is good practice to determine the size based on the total memory available on the device. For determining the available memory you can the MemoryClass. This is demonstrated by the following code.

int memClass = ( ( ActivityManager) activity.getSystemService( Context.ACTIVITY_SERVICE ) ).getMemoryClass();
int cacheSize = 1024 * 1024 * memClass / 8;
LruCache cache = new LruCache<String, Bitmap>( cacheSize );

3.2. Cleaning up your cache

As of API 14 you can override the onTrimMemory() method in Android components. This method is called by the Android system asking you to cleanup your memory in case the Android system requires resources for foreground processes.

Nothing listed.

If you need more assistance we offer Online Training and Onsite training as well as consulting