Optimizing the performance and user experience of Fire TV apps is a top priority for customers. One crucial aspect to consider is background memory usage, which can significantly impact your app’s start times and the overall user experience. While there’s no fixed threshold for background memory usage, it’s important to note Fire TV apps with higher background memory are the first in line to be cleared when Fire OS needs to free up resources. When an app has a “cold start”, all related processes, activities, views, and assets have to be loaded into memory. This load time affects how long it takes for users to get up and running with your app.
For this developer walkthrough on improving background memory usage, lets start with Android’s Principles of Navigation to better understand how they impact apps on a Fire OS device.
The following diagram shows four different states of the Fire App Builder sample app:
After first launch, the app goes from the Fire TV home screen to the app’s home screen and when the user selects content, the app transitions to the content details screen. If the user decides to play the content, the final view is the content playback screen. Keep in mind, if the user presses the remote control’s home button, the app is sent to background.
Android applications respond to default Activity lifecycle events such as “onPause” and may free up memory resources when the application is transitioning to background state. A common example is when users briefly pause a video in streaming app, press the home button, or inadvertently exit the app then want to quickly resume playback from where they left off. However, if an app stays in the background for an extended period, the operating system may shut it down, particularly if it's consuming a significant amount of background memory.
To address this issue, Android offers a system callback onTrimMemory that lets applications release memory in response to memory-related events at system level. In the context of Fire TV apps, onTrimMemory can be used to manage background memory usage and ensure optimal performance. Below is an example of onTrimMemory usage in the Fire TV app sample. Although the app is available only for Java, the examples include Kotlin samples that you may find useful for your Kotlin-based applicaitons.
This sample is a simple demonstration of how to react to memory-related events. Depending on your app’s architecture, you may choose from a variety of actions such as freeing up cache memory from data containers or unloading graphical resources that are not needed in background state. In addition, if an app has exhausted all attempts to free up memory and it is still getting callbacks for low memory, then it may be time to prepare for an unexpected shutdown by saving any application state that might be needed at this point.
Step 1: Download, build, and run Fire TV App Builder by following these instructions. Now that we have a running instance of sample app, we will make following changes.
Step 2: Add OnTrimMemory callback to ModularApplication
<path to sample app>/fire-app-builder-master/ModuleInterface/src/main/java/com/amazon/android/module/ModularApplication.java
Open ModularApplication.java and add oTrimMemory callback to this file. A reference example is shown in the code snippet below.
Java
import android.content.ComponentCallbacks2;
/**
* Base class for a modular application.
*/
public abstract class ModularApplication extends Application implements ComponentCallbacks2 {
...
...
...
...
@Override
public void onCreate() {
super.onCreate();
...
...
}
...
...
...
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
//This callback notifyies your app for memory related system callbacks
switch (level) {
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
/* The app is now transitioning to a background state*/
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
/* Low system memory callbacks when the application is in foreground.
In this blog we will skip this part */
break;
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
/*
Low Memory events when the application is in background.
Based on the message type you can decide what and how much memory
to release here.
*/
break;
default:
break;
}
}
}
Kotlin
import android.content.ComponentCallbacks2
/**
* Base class for a modular application.
*/
class ModularApplication : Application() , ComponentCallbacks2{
...
...
...
override fun onCreate() {
super.onCreate();
...
...
}
...
...
...
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
//This callback notifyies your app for memory related system callbacks
when (level) {
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
// The app is now transitioning to a background state
}
ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
/* Low system memory callbacks when the application is in foreground.
In this blog we will skip this part */
}
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
ComponentCallbacks2.TRIM_MEMORY_MODERATE,
ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
/*
Low Memory events when the application is in background.
Based on the message type you can decide what and how much memory
to release here.
*/
}
else -> {
}
}
}
}
Step 3: There are use cases in which navigating through an app's UI stacks one activity on top of another, resulting in a stack of multiple activities as shown in the state diagram above. We will store all activity references in a list variable in ModularApplication and also add a method for adding and removing entries. As part of this sample implementation, when the app needs to free memory in the background state, we will start releasing the memory by finishing all the activities except home screen. As the activities are destroyed, the associated memories will also be released.
Java
public abstract class ModularApplication extends Application implements ComponentCallbacks2 {
...
...
private List<Activity> arrayActivities = new ArrayList<>();
...
...
public void addActivity(Activity activity){
arrayActivities.add(activity);
}
public void removeActivity(Activity activity){
arrayActivities.remove(activity);
}
}
Kotlin
class ModularApplication : Application() , ComponentCallbacks2{
...
...
private var arrayActivities: ArrayList<Activity> = ArrayList ();
...
...
fun addActivity(activity: Activity) {
arrayActivities.add(activity);
}
fun removeActivity(activity: Activity) {
arrayActivities.remove(activity);
}
}
Step 4: Next we will make changes to the following list of files.
<path to sample app>/fire-app-builder-master/TVUIComponent/lib/src/main/java/com/amazon/android/tv/tenfoot/ui/activities/
ContentBrowseActivity.java
ContentDetailsActivity.java
ContentSearchActivity.java
FullContentBrowseActivity.java
SplashActivity.java
VerticalContentGridActivity.java
<path to sample app>/fire-app-builder-master/TVUIComponent/lib/src/main/java/com/amazon/android/uamp/ui/
PlaybackActivity.java
In all the above listed files we will modify Activitiy’s onCreate
and onDestroy
method. Following is an example of change done in each activity’s class.
Java
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.full_content_browse_activity_layout);
...
...
((ModularApplication)getApplication()).addActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
...
...
((ModularApplication)getApplication()).removeActivity(this);
}
Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.full_content_browse_activity_layout);
...
...
(application as ModularApplication).addActivity(this);
}
override fun onDestroy() {
super.onDestroy()
(application as ModularApplication).removeActivity(this);
}
Step 5: Next we will put a Debug log in ModularApplication.onTrimMemory and print the available memory for our application. The debug log will help in knowing the specific instances when OnTrimMemory is called and observe the memory consumptions and running activities during each call. We would also observe the app getting terminated when the system starts running low on resources.
Java
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
//This callback notifyies your app for memory related system callbacks.
switch (level) {
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
/* The app is now transitioning to a background state*/
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
/* Low system memory callbacks when the application is in foreground. In this blog we will skip this part */
break;
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
/*
Low Memory events when the application is in background.
Based on the message type you can decide what and how much memory
to release here.
*/
Log.e("onTrimMemory", "Total activities :" + arrayActivities.size());
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
activityManager.getMemoryInfo(mi);
double sizeInMB = mi.availMem / 0x100000L;
Log.e("onTrimMemory", "Available Memory:" + sizeInMB);
break;
default:
break;
}
}
Kotlin
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
//This callback notifyies your app for memory related system callbacks
when (level) {
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
// The app is now transitioning to a background state
}
ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
/* Low system memory callbacks when the application is in foreground.
In this blog we will skip this part */
}
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
ComponentCallbacks2.TRIM_MEMORY_MODERATE,
ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
/*
Low Memory events when the application is in background.
Based on the message type you can decide what and how much memory
to release here.
*/
Log.e("onTrimMemory", "Total activities :" + arrayActivities.size)
val mi = ActivityManager.MemoryInfo()
val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
activityManager.getMemoryInfo(mi)
val sizeInMB = (mi.availMem / 0x100000L).toDouble()
Log.e("onTrimMemory", "Available Memory:$sizeInMB")
}
else -> {
}
}
}
Step 6: Now launch the sample app (by clicking on the home screen icon or through Android Studio) and play some sample videos. While testing, you can open a Terminal window (Terminal on MAC and command console on Windows) and enter the following command to keep a watch on logcat.
adb logcat | grep "OnTrimMemory"
Step 7: While video is playing, press the home button to send application to background. Now open another app like, Prime Video, and play some HD content, open a few other apps, and do the same until you observe the following lines in logcat.
03-24 22:21:35.587 29110 29110 E onTrimMemory: Total activities :3
03-24 22:21:42.462 29110 29110 E onTrimMemory: Available Memory :408.0
Step 8: In the steps above, we observed our sample app getting killed in background when OS is running low on resources. How can we make our app stay longer in background state by moving further down the rank of resource-intensive background apps? One simple change we can do in the sample app is check the size of activity list and destroy all activities other than home screen (topmost activity). By freeing up these activities, we will also free up the associated resources and reduce the overall memory consumption by our sample app. Following is a sample code snippet that frees up the list of activities from the stack except the topmost activity (home screen).
Java
public void onTrimMemory(int level) {
...
...
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
while(arrayActivities.size() > 1){
Activity activity = arrayActivities.get(arrayActivities.size() - 1);
arrayActivities.remove(activity);
activity.finish();
}
...
...
}
Kotlin
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
...
...
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
ComponentCallbacks2.TRIM_MEMORY_MODERATE,
ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
while (arrayActivities.size > 1) {
val activity = arrayActivities[arrayActivities.size - 1]
arrayActivities.remove(activity)
activity.finish()
}
....
....
}
...
...
}
The above sample implementation for freeing up background memory lets the user resume a video playback if they return to an application within a short span of time. If the application stays in background for prolonged duration and receives low memory callbacks from OS, then it starts freeing up memory by releasing activities for video playback and video details page. When the user returns to the app and brings it to the foreground, they will see the application home screen and can continue to navigate further from that point. As mentioned in the beginning of this section, each app has a different design and depending on the design an app can take actions which are best suited to reduce background memory consumption in response to system-initiated memory callback events.
We hope you are able to optimize your Fire TV apps on behalf of customers using your products. By taking advantage of these Fire OS features, you should find your loading times and app states to be performant and quick.