Optimize memory usage

Memory optimization is crucial for ensuring smooth performance, preventing app crashes, and maintaining system stability and platform health. While memory usage should be monitored and optimized in every app, content apps for TV devices have specific challenges that differ from typical Android apps for handheld devices.

High memory consumption can lead to problems with app and system behaviors including:

  • The app itself can become slow or laggy, or in the worst case, get killed.
  • User-visible system services (Volume Control, Picture Settings dashboard, Voice Assistant, etc.) become very laggy or may not work at all.
  • System components can get killed; these components then restart, triggering spikes of extreme resource contention and directly impacting the foreground app.
  • Transition to the Launcher can be significantly delayed, and leave the foreground app appearing unresponsive until the transition finishes.
  • The system may enter into a direct reclaim situation, temporarily pausing a threads execution whilst waiting for memory allocation. This can happen to any thread such as the main thread or codec related threads potentially causing audio and video frame drops, and UI glitches.

Memory considerations on TV Devices

TV devices typically have considerably less memory than phones or tablets. For example, a configuration we can see on TV is 1 GB of RAM and 1080p video resolution. At the same time, most TV apps have similar features; therefore similar implementation and common challenges. These two situations present problems not seen in other device types and apps:

  • Media TV apps are usually composed of both gridded image views and fullscreen background images which require loading a lot of images into memory in a short period of time
  • TV apps play multimedia streams which require to allocate a certain amount of memory to play video and audio and need considerable media buffers to ensure smooth playback.
  • Additional media features (seeking, episode change, audio track change, etc.) can take additional memory pressure if not implemented properly.

Understand TV devices

This guide primarily focuses on app memory usage and memory targets for low-RAM devices.

On TV devices, consider these characteristics:

  • Device memory: The amount of Random Access Memory (RAM) the device has installed.
  • Device UI resolution: The resolution the device uses to render the OS and Applications UI; this is typically lower than the device video resolution.
  • Video resolution: The maximum resolution the device can play videos at.

This leads to categorizing different device types and how memory should be used by them.

TV devices summary

Device memory Device video resolution Device UI resolution isLowRAMDevice()
1 GB 1080p 720p Yes
1.5 GB 2160p 1080p Yes
≥1.5 GB 1080p 720p or 1080p No*
≥2 GB 2160p 1080p No*

Low-RAM TV devices

These devices are in a memory constrained situation and will report ActivityManager.isLowRAMDevice() to true. Applications which are running on low-RAM TV devices need to implement additional memory control measures.

We consider devices with the following characteristics to fall into this category:

  • 1 GB devices: 1 GB of RAM, 720p/HD (1280x720) UI resolution, 1080p/FullHD (1920x1080) Video resolution
  • 1.5 GB devices: 1.5 GB of RAM, 1080p/FullHD (1920x1080) UI resolution, 2160p/UltraHD/4K (3840x2160) Video resolution
  • Other situations in which the OEM defined the ActivityManager.isLowRAMDevice() flag due to additional memory constraints.

Regular TV devices

These devices don't suffer such a significant memory pressure situation. We consider these devices to have the following characteristics:

  • ≥1.5 GB of RAM, 720p or 1080p UI and 1080p video resolution
  • ≥2 GB of RAM, 1080p UI and 1080p or 2160p video resolution

This doesn't mean apps shouldn't care about memory usage on these devices, as some specific memory misuse can still exhaust available memory and perform poorly.

Memory targets on low-RAM TV devices

When measuring memory on these devices, we strongly recommend monitoring every section of the memory using the Android Studio memory profiler. TV apps should profile their memory usage and work to put their categories below the thresholds we define in this section.

memory profiler

On the How memory is counted section you'll find a detailed explanation of the reported memory figures. For the definition of thresholds for TV apps, we will focus on three memory categories:

  • Anonymous + Swap: Composed of Java + Native + Stack allocation memory in Android Studio.
  • Graphics: Directly reported on the profiler tool. Generally composed of graphics textures.
  • File: Reported as "Code" + "Others" categories in Android Studio.

With these definitions, the following table indicates the maximum value each type of memory group should use:

Memory type Purpose Usage targets (1 GB)
Anonymous + Swap (Java + Native + Stack) Used for allocations, media buffers, variables, and other memory-intensive tasks. < 160 MB
Graphics Used by the GPU for textures and display related buffers 30-40 MB
File Used for code pages and files in memory. 60-80 MB

The maximum total memory (Anon+Swap + Graphics + File) must not exceed the following:

  • 280 MB of total memory usage (Anon+Swap + Graphics + File) for 1 GB low-RAM devices.

It is strongly recommended not to exceed:

  • 200 MB of memory usage on (Anon+Swap + Graphics).

File memory

As general guidance for file backed memory be aware that:

  • In general file memory is handled well by OS memory management.
  • We have not found it to be a major cause of memory pressure at this moment.

However, when dealing with File memory in general:

  • Don't include unused libraries into your build, and use small subsets of libraries rather than the complete ones when possible.
  • Don't keep large files opened into memory and release them as soon as you are done with them.
  • Minimize your compiled code size for Java and Kotlin classes, see the Shrink, obfuscate, and optimize your app guide.

Specific TV recommendations

This section provides specific recommendations for optimizing memory usage on TV devices.

Graphics memory

Use appropriate image formats and resolutions.

  • Do not load images with higher resolution than the device UI resolution. For example, 1080p images should be downsized to 720p on a 720p UI device.
  • Use hardware-backed bitmaps when possible.
    • On libraries like Glide, enable the Downsampler.ALLOW_HARDWARE_CONFIG feature which is disabled by default. Enabling this avoids duplicating bitmaps which otherwise would be in both graphics memory and anonymous memory.
  • Avoid intermediate renders and re-renders
    • These can be identified with Android GPU Inspector:
    • Look on the "Textures" section for images that are steps towards the final render rather than being only the elements forming them, this is commonly a so called "intermediate render".
    • For Android SDK applications you can often remove these by using the layout flag forceHasOverlappedRendering:false to disable intermediate renders for this layout.
    • See Avoid Overlapping Renders on overlapping renders as a great resource.
  • Avoid loading placeholder images when possible, use @android:color/ or @color for placeholder textures.
  • Avoid compositing multiple images on the device when the composition could be performed offline. Prefer to load standalone images rather than doing image composition from downloaded images
  • Follow the Handling bitmaps guide to better deal with Bitmaps.

Anon+Swap memory

Anon+Swap is composed of Native + Java + Stack allocations in Android Studio memory profiler. Use ActivityManager.isLowMemoryDevice() to check if the device is memory constrained, and adapt to this situation following these guidelines.

  • Media:
    • Specify a variable size for media buffers depending on the device RAM and video playback resolution. This should account for 1 minute of video playback:
      1. 40-60 MB for 1 GB / 1080p
      2. 60-80 MB for 1.5 GB / 1080p
      3. 80-100 MB for 1.5 GB / 2160p
      4. 100-120 MB for 2 GB / 2160p
    • Free media memory allocations when changing an episode to prevent increases in the total amount of Anonymous memory.
    • Release and stop media resources immediately when you app gets stopped: Use the activity lifecycle callbacks to handle audio and video resources. If you are not an audio app, stop your playback when onStop() happens on your activities, save all the work you are performing and set your resources to be released. To schedule work you may need later. See the Jobs and Alarms section.
    • Pay attention to the buffer's memory when video seeking: Developers often allocate additional 15-60s of future content when seeking to have video ready for the user, but this creates additional memory overhead. In general, don't take more than 5s of future buffer until the user has selected the new video position. If you strictly need to pre-buffer additional time while seeking, make sure to:
      • Allocate the seeking buffer ahead of time and reuse it.
      • The buffer size should be no bigger than 15-25 MB (depending on the device memory).
  • Allocations:
    • Use the graphics memory guidance to ensure you don't duplicate images in Anonymous memory
      • Images are often the biggest user of memory so duplication of them can put a lot of pressure on the device. This is especially true during heavy navigation of image gridviews.
    • Release allocations by dropping their references when moving screens: Ensure there are no references to bitmaps and objects left behind.
  • Libraries:
    • Profile memory allocations from libraries when adding new ones, as they may load additional libraries as well, which may also make allocations and create Bindings.
  • Networking:
    • Do not perform blocking network calls during app startup, they slow down the application startup time and create additional memory overhead at launch, where memory is particularly constrained by the app load. Show a loading or splash screen first and do network requests once the UI is in place.

Bindings

Bindings introduce additional memory overhead as they bring other applications into memory or increase the memory consumption of the bound app (if it is in memory already) to facilitate the API call. This, as a result, reduces the available memory for the foreground app. When binding a service, be mindful of when and how long you are using the binding. Make sure to release the binding as soon as it's not needed.

Typical bindings and best practices:

  • Play integrity API: Used to check for device integrity
    • Check for device integrity after the loading screen and before media playback
    • Release references to PlayIntegrity StandardIntegrityManager before playing content.
  • Play Billing Library: Used for managing subscriptions and purchases using Google Play
  • GMS FontsProvider
    • Prefer to use standalone fonts on low-RAM devices rather than using fonts provider, as downloading the fonts is costly and FontsProvider will bind services to do it.
  • Google Assistant library: Sometimes used for search and in-app search, if possible, replace this library.
    • For leanback apps: Use Gboard text to speech or androidx.leanback library.
      • Follow Search guidelines for implementing search.
      • Note: leanback is deprecated and apps should move to TV Compose.
    • For Compose apps:
      • Use Gboard text to speech to implement voice search.
    • Implement Watch Next to make media content in your app discoverable.

Foreground Services

Foreground Services are a special type of service which is tied to a notification. This notification is displayed on the notification tray on phones and tablets, but TV devices don't have a notification tray in the same sense as those devices. Even if Foreground Services are useful because they can be kept running while the application is in the background, TV apps must follow these guidelines:

In Android TV and Google TV, Foreground Services are only allowed to keep running once the user leaves the app:

  • For audio apps: Foreground Services are only allowed to keep running once the user leaves the app to keep playing the audio track. The service must be stopped immediately after audio playback ends.
  • For any other app: all Foreground Services must be stopped once the user leaves your app, as there is no notification to notify the user that the app is still running and consuming resources.
  • For background jobs such as updating recommendations or Watch Next, use WorkManager.

Jobs and Alarms

WorkManager is the state-of-the-art Android API for scheduling background recurring jobs. WorkManager will use the new JobScheduler when available (SDK 23+) and the old AlarmManager when it's not. For best practices performing scheduled jobs on TV follow these recommendations:

  • Avoid using the AlarmManager APIs, on SDK 23+, especially AlarmManager.set(), AlarmManager.setExact() and similar methods, as they don't allow the system to decide the proper time to run the jobs (as an example, when the device is idling).
  • On low-RAM devices, avoid running jobs unless strictly necessary. If needed, use WorkManager WorkRequest only for updating recommendations after playback, and try to do so while the app is still open.
  • Define WorkManager Constraints to let the system run your jobs when the time is appropriate:

Kotlin

Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresStorageNotLow(true)
.setRequiresDeviceIdle(true)
.build()

Java

Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresStorageNotLow(true)
.setRequiresDeviceIdle(true)
.build()
  • If you must run jobs regularly (for example, to update Watch Next based on a user's content watching activity in your app on another device), then keep memory use down keeping the job's memory consumption below 30 MB.

Other general guidelines

Following guidelines provide general information on Android App development:

  • Minimize object allocations, optimize object re-use and de-allocate any unused objects promptly.
    • Do not hold references to objects, especially bitmaps.
    • Avoid using System.gc() and direct release memory calls as they interfere with the system's memory handling process: As an example, in devices using zRAM, a forced call to gc() can temporarily increase memory usage due to the compression and decompression of the memory.
    • Use LazyList such as demonstrated in a catalog browser in Compose or RecyclerView in the now deprecated Leanback UI toolkit to reutilize views and not re-create list elements.
    • Cache locally elements read from external content providers which are unlikely to change and define updating intervals that prevent allocating additional external memory.
  • Check for possible memory leaks.
    • Watch out for typical memory leak cases such as references inside anonymous threads, reallocation of video buffers which never get released, and other similar situations.
    • Use heap dump to debug memory leaks.
  • Generate baseline profiles to minimize the amount of just-in-time compilation is needed when executing your app on a cold start.

Tools summary