65% smaller APK and 70% less memory: How to optimize the memory of my Android App

65% smaller APK and 70% less memory: How to optimize the memory of my Android App

(Note: This is a translation of the provided title)

Why is application memory important?

Efficient applications that use minimal memory improve performance, conserve device resources, and extend battery life. They provide a smooth user experience and are more popular in app stores. Such applications are compatible with various devices.

Methods to track memory

We can track an application’s memory using any of the following methods:

1. ADB command

To get an application’s memory at a specific moment, run the following command in the terminal:

adb shell dumpsys meminfo appPackageName

Note: Replace appPackageName’ with the actual package name of the application you want to monitor.

The example screenshot above shows that the memory usage of the application is 42MB.

Advantages:
Get the memory of any application in the device.
Limitations:
There is no way to chart memory changes based on user interaction like Android Profiler can.

2. Android Profiler

Steps to start Profiler

View (in top pane) -> Tool Windows -> Profiler -> Click ” + ” -> Select Devices and Packages

Advantages:
Android Profiler can track runtime behavior and memory consumption based on application usage.
It provides a comprehensive view of application memory.
Limitations:
Profiling can only be done on “debuggable applications”.

Note: Please note that memory analysis is not covered in this document. For more in-depth information, see the official documentation.

Our app is pretty heavy on images, and we’ve noticed that after viewing 47 images, the app’s memory usage exceeded 500MB (as shown in the screenshot above). This increases the risk of encountering an out-of-memory exception (OOM). We recognized the need for memory optimization to improve user experience and took the following steps. let’s start…

Memory optimization measures

1. Pixel color format change

Pixel Color Format: The pixel color format, also known as the pixel format, specifies how the color information for each pixel in the image is stored in memory. It defines the arrangement of red, green, blue, and alpha (transparency) components, affecting color quality and rendering performance.

In Android, multiple color formats are supported. However, let’s focus on the most commonly used ones below.

ARGB_8888 (32 bits per pixel) – 8 bits for alpha (transparency), 8 bits for red, 8 bits for green, 8 bits for blue

RGB_565 (16 bits per pixel) – 5 bits for red, 6 bits for green, 5 bits for blue, no alpha

As shown in the image above, the difference between RGB 565 and ARGB 8888 is almost imperceptible, but in RGB 565 the memory is reduced by about 50%.

We use the Glide library for image rendering. By default, Glide uses the ARGB 8888 pixel color format for image loading. However, Glide provides the flexibility to configure your preferred pixel color format.

@GlideModule
class CustomGlideModule : AppGlideModule() {<!-- -->
    override fun applyOptions(context: Context, builder: GlideBuilder) {<!-- -->
        builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
    }
}

As shown above, we adjust the default color format to RGB 565 at the application level. This configuration change resulted in a 50% reduction in memory consumption per pixel. For projects that rely on images, this decision is very important.

Note: RGB 565 has certain limitations, such as slight color changes and no support for transparency. This may result in reduced color accuracy for a particular image, so the choice should be made based on the specific requirements of the application.

2. Glide DiskCacheStrategy changes

DiskCacheStrategy mainly determines how images are cached on the device.

Previously, we were using “DiskCacheStrategy.All”. However, after doing some research, we realized that “DiskCacheStrategy.Resource” better suited our specific needs.

DiskCacheStrategy.ALL -> Cache all versions of an image.

DiskCacheStrategy.Resource-> Glide only caches the final image on disk after all transformations (e.g. resizing, cropping). This strategy is ideal when you want to cache fully processed images rather than raw data.

Other DiskCacheStrategies can be referenced in the official documentation

For example, in applications like WhatsApp, images are often displayed in a compressed format. However, occasionally users may want to share these images in their original high-resolution state. In this case DiskCacheStrategy.Resource will not apply.

Therefore, the choice of diskCacheStrategy depends on the specific requirements of the application.

** 3. Modify offscreenPageLimit **

Initially to minimize latency and prevent blank screens, we configured offscreenPageLimit to 3 for the ViewPager. Our assumption is that ViewPager will cache the previous page, current page and next page. However, upon deeper investigation, we discovered that it actually cached the first three pages, the current page, and the next three pages, which resulted in a total of 7 large high-definition images being stored.

Based on this analysis, we chose to reduce offscreenPageLimit to 1. This not only reduces memory usage but also makes the application run smoothly without any latency issues.

viewPager.offscreenPageLimit = 1

4. Clear cache when onViewRecycled

onViewRecycled is the callback method in RecyclerView.Adapter, which is triggered when the view is recycled. It is often used with Glide to suppress loading images, optimizing memory and network usage.

override fun onViewRecycled(holder: ChildBingeHolder) {<!-- -->
        GlideApp.with(context).clear(yourView)
}

Clearing the view cache when recycling views in the adapter helps free up memory.

5. Specify image size

We have a 64×64 pixel (small) ImageView, but the API provides a 512×512 pixel (large) image. Decoding such a large image for a small placeholder is inefficient in terms of memory usage and application performance.

To solve this problem, we specify the width and height of the view to ensure that the image is scaled correctly rather than loading an overly large image into memory.

Glide.with(this)
        .load(IMAGE_URL)
        .override(targetWidth, targetHeight)
        .into(imageView)

Using override() can greatly reduce memory consumption by specifying the desired image size, which is especially useful when working with large images or displaying multiple images simultaneously.

6. Handling onTrimMemory

Implement onTrimMemory(int) to gradually release memory based on system constraints. This improves system responsiveness and user experience by keeping your processes active longer. Without resource pruning, the system may kill your caching process, requiring your application to restart and restore state when the user returns.

NOTE: The following memory level processing is customized for the specific requirements of our application. We are customizing memory levels as needed. Memory level documentation

override fun onTrimMemory(level: Int) {<!-- -->
    //Memory Levels documentation: https://developer.android.com/reference/android/content/ComponentCallbacks2
    // TRIM_MEMORY_COMPLETE & amp; TRIM_MEMORY_MODERATE are the levels which are called when the app is in background
    if (level == android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {<!-- -->
        GlideApp.get(this).clearMemory()
    } else if (level == android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE) {<!-- -->
        GlideApp.with(this).onTrimMemory(TRIM_MEMORY_MODERATE)
    }
}

After implementing the above steps and making some minor adjustments, we managed to achieve significant improvements in application memory management.

In the provided screenshot we can observe the memory behavior. It clearly shows that the memory remains unchanged and any unused memory is efficiently managed during constant scrolling of the image.

result:

As shown in the table above, memory usage decreased from 515MB when browsing 47 images to 137MB when browsing 67 images (over 70% reduction). This improvement allows us to add more images to the application without worrying about memory limitations.

Conclusion

In summary, our journey to optimize the app demonstrates the significant impact of reducing APK size and optimizing memory. These efforts not only improve the user experience, but also pave the way for more efficient application performance, allowing us to deliver better, faster, and smoother applications to our customers.