65% Smaller APKs and 70% Less Memory: How I Optimized My Android App -Part II (App Memory)

Tarun Anchala
6 min readOct 17, 2023

--

Photo by Kammeran Gonzalez-Keola: https://www.pexels.com/photo/surfer-riding-foamy-sea-waves-on-surfboard-7925914/

As part of the App optimization series, we have covered APK size reduction in “65% Smaller APKs and 70% Less Memory: How I Optimized My Android App -Part I(APK Size)

In this blog, we will explore memory optimization in depth.

Why App Memory is Important?

Efficient apps that use minimal memory boost performance, save device resources and extend battery life. They provide a smooth user experience and are preferred in app stores. Such apps are compatible with a wide range of devices.

Methods for Tracing Memory

We can follow any of the following approaches to track an app’s memory:

1. ADB command

To fetch app memory at a specific instance, run the following command in the terminal:

adb shell dumpsys meminfo appPackageName

Note: Replace ‘appPackageName’ with the actual package name of the app you want to monitor

The sample screenshot above indicates that the app’s memory usage is 42MB.

Advantage:

  • Fetch Memory of any app in the device.

Limitation:

  • It’s not feasible to monitor memory changes in the form of graphs based on user interactions like the Android Profiler allows.

2. Android profiler

Steps to Launch Profiler

View(on Top Pane) -> Tool Windows -> Profiler -> Click “+” -> select device & package

Advantage:

  • The Android Profiler allows tracking runtime behavior and memory consumption based on app usage.
  • It provides a comprehensive view of your app’s memory.

Limitation:

  • Can only profile “debuggable apps”.

Note: Kindly note that we won’t be covering memory analysis within this blog. For in-depth information, please refer to the official documentation available here.

Our app focuses heavily on images, and we’ve noticed that the app’s memory usage exceeded 500MB (as shown in the screenshot above) after scrolling through 47 images. This raises the risk of encountering Out-of-Memory Exceptions (OOM). We recognized the need for Memory optimization to enhance the user experience and have taken the following steps. Let’s get started…

Photo by Braden Collum on Unsplash

1. Pixel Color Format change

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

In Android, several 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, and 8 bits for blue

RGB_565 (16 bits per pixel) — 5 bits for red, 6 bits for green, and 5 bits for blue, No Alpha

As we see in the image above, the difference between RGB 565 and ARGB 8888 is hardly noticeable but memory is reduced by ~ 50% in RGB 565.

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

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

As displayed above, we have adjusted the default color format to RGB 565 at the app level. This configuration change results in a 50% reduction in memory consumption per pixel. For projects that heavily rely on images, this decision holds significant importance.

Note: RGB 565 has certain limitations, such as slight color variations and a lack of transparency support. This may result in reduced color accuracy for specific images, so the choice should be made based on the specific requirements of the app.

2. Glide DiskCacheStrategy Change:

The DiskCacheStrategy primarily determines how your images will be cached on the device.

Earlier, we were utilizing “DiskCacheStrategy.All”. However, after conducting some research, we realized that “DiskCacheStrategy.Resource” is more aligned with our specific needs.

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

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

Additional DiskCacheStrategies can be referenced in the official documentation

For instance, in applications like WhatsApp, images are often displayed in a compressed format. But occasionally, users might want to share these images in their original, high-resolution state. In such cases, DiskCacheStrategy.Resource wouldn’t be suitable.

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

3. Modify offscreenPageLimit:

Initially, to minimize latency and prevent blank screens, we configured the offscreenPageLimit to 3 for the ViewPager. Our assumption was that the ViewPager would cache the previous, current, and next pages. However, upon further investigation, we discovered that it was actually caching the previous three, current, and next three pages, resulting in a total of 7 large-size high-definition images being stored in memory.

Based on this analysis, we opted to reduce the offscreenPageLimit to 1. This not only reduced memory usage but also resulted in the app operating smoothly without any latency concerns.

viewPager.offscreenPageLimit = 1

4.Clear cache on onViewRecycled

onViewRecycled is a callback method in RecyclerView.Adapter that is triggered when a view is being recycled. It's commonly used with Glide to cancel ongoing image loading for the recycled view, optimizing memory and network usage.

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

Clearing the view cache when views are recycled in the adapter aids in freeing up memory.

5.Specify Image size

We had a 64x64 px (Small) ImageView, but our API was providing a 512x512 px(large) image. Decoding such large image for a small placeholder is inefficient in terms of memory usage and app performance.

To address this, we specified the width and height of the view to ensure the image is correctly scaled, rather than loading the oversized 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 dimensions, which is especially useful when handling large images or multiple images displayed simultaneously.

6. Handle onTrimMemory

Implement onTrimMemory(int) to release memory incrementally based on system constraints. This improves system responsiveness and user experience by keeping your process alive longer. Without resource trimming, the system may kill your cached process, requiring your app to restart and restore the state when the user returns.

Note: The following memory level handling is tailored to meet our app’s specific requirements. We are customizing memory levels as needed. Memory Levels documentation

override fun onTrimMemory(level: Int) {
//Memory Levels documentation : https://developer.android.com/reference/android/content/ComponentCallbacks2
// TRIM_MEMORY_COMPLETE & 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 mentioned steps and making some minor adjustments, we successfully achieved significant improvements in app memory management.

In the provided screenshot, we can observe memory behavior. It clearly indicates that memory remains constant, and any unused memory is effectively managed during continuous image scrolling.

Outcome:

Memory usage decreased from 515MB while scrolling through 47 images to 137MB (more than 70% reduction) while scrolling through 67 images as shown in the above table. This improvement allows us to add more images to the app without having concerns about memory constraints.

Conclusion:

In summary, our journey of optimizing the App has demonstrated the significant impact of reducing APK size and optimizing memory. These efforts have not only improved user experience but also paved the way for more efficient app performance, allowing us to deliver a better, faster, and more streamlined application to clients.

Thanks for Reading…

Photo by Markus Spiske on Unsplash

--

--