Improve Your App's Performance with Bitmap Downsampling

Improve Your App's Performance with Bitmap Downsampling

Technical quality report: Release 12 (12.0)

Google Play recommendation:
Improve your app's performance with bitmap downsampling.

Images are an important part of modern Android applications. However, loading a high-resolution image directly into memory can consume unnecessary resources, slow down the application, and potentially cause an OutOfMemoryError.

This Google Play recommendation normally means that the application is loading one or more images at a higher resolution than required by the screen, thumbnail, or ImageView.

What Is Bitmap Downsampling?

Bitmap downsampling is the process of decoding an image at a smaller resolution before loading it into memory.

For example, suppose an image has a resolution of 4000 × 3000 pixels, but the application displays it inside a 400 × 300 pixel area. Loading the original image wastes memory because most of its pixels are not visible to the user.

Simple analogy: It is like transporting a billboard-sized photograph just to display it on a small business card. The image should first be resized according to its actual display area.

Why Is Downsampling Important?

An Android bitmap using ARGB_8888 normally requires approximately four bytes for every pixel.

The estimated memory requirement can be calculated as follows:

Bitmap memory = width × height × 4 bytes

A 4000 × 3000 image may therefore require:

4000 × 3000 × 4 = 48,000,000 bytes

That is approximately 48 MB of memory for only one image. If several large images are loaded simultaneously, the application may experience:

  • Slow image loading
  • High memory consumption
  • Frozen or unresponsive screens
  • Excessive garbage collection
  • Poor scrolling performance
  • OutOfMemoryError
  • Unexpected application crashes

Downsampling reduces the image resolution before allocating the bitmap, resulting in lower memory usage and improved application performance.

Solution 1: Use BitmapFactory.Options

Android provides BitmapFactory.Options to inspect an image's dimensions and calculate an appropriate sampling size before decoding it.

Calculate the Sampling Size

fun calculateInSampleSize(
    options: BitmapFactory.Options,
    requiredWidth: Int,
    requiredHeight: Int
): Int {
    val imageHeight = options.outHeight
    val imageWidth = options.outWidth
    var inSampleSize = 1

    if (imageHeight > requiredHeight ||
        imageWidth > requiredWidth
    ) {
        val halfHeight = imageHeight / 2
        val halfWidth = imageWidth / 2

        while (
            halfHeight / inSampleSize >= requiredHeight &&
            halfWidth / inSampleSize >= requiredWidth
        ) {
            inSampleSize *= 2
        }
    }

    return inSampleSize
}

Decode the Downsampled Bitmap

fun decodeSampledBitmapFromFile(
    filePath: String,
    requiredWidth: Int,
    requiredHeight: Int
): Bitmap? {
    val options = BitmapFactory.Options().apply {
        inJustDecodeBounds = true
    }

    // Read the image dimensions without allocating
    // memory for the complete bitmap.
    BitmapFactory.decodeFile(filePath, options)

    options.inSampleSize = calculateInSampleSize(
        options,
        requiredWidth,
        requiredHeight
    )

    // Decode the appropriately sampled image.
    options.inJustDecodeBounds = false

    return BitmapFactory.decodeFile(filePath, options)
}

The function can then be used as follows:

val bitmap = decodeSampledBitmapFromFile(
    filePath = imagePath,
    requiredWidth = 800,
    requiredHeight = 600
)

imageView.setImageBitmap(bitmap)

Setting inJustDecodeBounds to true allows the application to inspect the image dimensions without allocating memory for the complete bitmap.

Solution 2: Use Coil

For modern Android projects, an image-loading library such as Coil can perform image resizing, memory management, and caching automatically.

imageView.load(imageUrl) {
    size(800, 600)
    crossfade(true)
}

For Jetpack Compose:

AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(imageUrl)
        .size(800, 600)
        .crossfade(true)
        .build(),
    contentDescription = "Optimised image",
    contentScale = ContentScale.Crop,
    modifier = Modifier.size(
        width = 400.dp,
        height = 300.dp
    )
)

The image should be requested at approximately the same size as the UI component displaying it.

Solution 3: Use Glide

Glide can also resize and cache images efficiently:

Glide.with(imageView.context)
    .load(imageUrl)
    .override(800, 600)
    .centerCrop()
    .into(imageView)

The override() method instructs Glide to load the image using the specified dimensions instead of decoding an unnecessarily large version.

Additional Image Optimisation Recommendations

1. Resize Bundled Images

Images stored inside the application should be resized before being placed in the following directory:

res/drawable

Avoid including a 4000-pixel image if the application only displays it as a small icon or thumbnail.

2. Use WebP Where Appropriate

WebP can reduce an application's image file size while maintaining acceptable visual quality. However, file compression and bitmap downsampling solve two different problems:

  • Compression reduces storage and download size.
  • Downsampling reduces decoded image dimensions and memory usage.

An image can have a small compressed file size but still consume significant memory after being decoded.

3. Avoid Decoding Full-Resolution Images

Avoid loading an image directly using code such as:

val bitmap = BitmapFactory.decodeFile(imagePath)
imageView.setImageBitmap(bitmap)

This code decodes the complete image without considering the dimensions of the target view.

4. Load Thumbnails for Image Lists

For a RecyclerView, gallery, or media browser, load thumbnail-sized images instead of full-resolution photographs. The original image should only be loaded when the user opens the detailed view.

5. Test on Lower-Memory Devices

An application may work correctly on a high-end development phone but crash on a device with limited memory. Testing should therefore include entry-level and older Android devices.

How to Verify the Improvement

  1. Build a new release of the application.
  2. Test every screen that displays large images.
  3. Monitor memory usage using Android Studio Profiler.
  4. Scroll through image-heavy lists repeatedly.
  5. Check Logcat for OutOfMemoryError.
  6. Upload the new Android App Bundle to Google Play Console.
  7. Monitor the technical quality report after Google processes the release.
Important: The Google Play recommendation may not disappear immediately. Play Console may require additional processing time and sufficient usage data to reassess the new release.

Conclusion

Bitmap downsampling is a simple but important optimisation for Android applications. The application should decode an image based on the dimensions required by the user interface instead of always loading the full-resolution file.

For Release 12 (12.0), implementing appropriate image resizing through BitmapFactory, Coil, or Glide can reduce memory consumption, improve scrolling performance, prevent crashes, and provide a smoother experience across different Android devices.

For additional technical information, refer to the official Android Developers documentation on loading large bitmaps efficiently .


Suggested labels: Android Development, App Performance, Bitmap, Google Play Console, Kotlin, Mobile Development

Improve Your App's Performance with Bitmap Downsampling Improve Your App's Performance with Bitmap Downsampling Reviewed by Admin on 11:18 PM Rating: 5

No comments:

Powered by Blogger.