Memory Issues on Android TV App and How I fixed them

Fitzgerald Afful
5 min readOct 25, 2022

--

Background:

A couple of months ago, I worked with Async Art where I worked on fascinating projects and this is one of a few I documented. Async Art is a digital platform (web/tv apps) for programmable art. Artists upload their cool artwork, and it's displayed, bid on and sold on the platform. Artists can also mint NFTs and others, but that's outside this article's domain.

A programmable art (referred to as artwork from here on) can be autonomous. This means they can change hourly, on a half-day basis or three times a day. Inside the Android TV app, these artworks switch to their new versions when the time is up.

Problem Statement(s):

Artworks are cached on the app to prevent re-downloading after swiping through them. Caching these artworks has been a problem because there are a lot of artworks and these are averagely 15MB in size. Also because they would be displayed on TVs, their sizes cannot be reduced as they would appear pixelated. Swiping through artworks on full screen often causes an Out of Memory Exception with the average memory consumption after swiping through 20 images of 3GB.

The UI Setup is an Activity with a viewpager of fragments

Solutions tried:

  • Increasing Heap Size: This is the first thing every Stackoverflow answer suggests. Didn't help at all. android:largeHeap="true" in the Manifest file didn't change anything. It is understood what this seeks to do; but this didn't solve the problem.
  • Replacing ViewPager: Android's ViewPager (VP) and FragmentStatePagerAdapter have their way of loading views before you get to them. Using it meant the views on either side of the current view had to be loaded (described here). In theory, it seems like a great idea. However, it contributed to the size of the memory consumed in practice. Replacing the VP with a single FragmentContainer helped. So instead of a Viewpager with Fragments, we have one activity with a FragmentContainer for the current Fragment. This reduced the leaks by some margin (though not very much). This idea came from the tvOS implementation of this app.
  • Garbage Collecting Manually: Using Android Studio's Profiler and LeakCanary, it was evident that the views in the Fragment were retained when they got leaked. The Garbage Collector wasn't removing the elements inside the Fragment (I assumed this because when onDestroyView was called, the memory usage graph didn't decrease). To garbage-collect manually, all views and properties in the DetailFragment were set to null in OnDestroy before callingRuntime.getRuntime().gc(); This didn’t look optimal but the leaks also reduced by some margin too.
  • Fragment Manager's Transactions and Backstack: One of the things I tried, was to create my own form of view pager: 3 fragments in an Activity so I could move and update them manually. It turns out that Fragments cannot be moved from one container to another. Re: IllegalStateException: Can't change container ID of Fragment. They are attached to the container. I also realized that while fragments were created, they were added to the back stack whether you chose addToBackStack(null)or not. And one way to fix this was to pop any extra fragments after adding a new fragment, to keep the back stack size at exactly 2.
  • Switching from Bitmaps to Drawables: To save memory after using bitmaps, they had to be recycled manually by calling the bitmap.recycle() method. This answer explains why you should recycle your bitmaps manually. What happened after this was a bug, "Canvas: trying to use a recycled bitmap". This compelled me to switch from using Bitmaps to Drawables. It needs to be said that you can still experience the recycled bitmap error after switching since for example, you can't blur a drawable. You’d have to convert the drawable into a bitmap before blurring.
  • Hash-map to Store Drawables and Pre-load them: A LinkedHashmap was created in the Activity to cater for easy loading. When the user swipes to the next artwork, the following image is downloaded and stored in the LinkedHashmap using the image’s URL as the key. There is a check for whether this hash map contains the drawable for the image URL in the Fragment. If it does, the drawable is being used. If it doesn't, the image is downloaded and stored. To prevent this LinkedHashmap from holding many items, the maximum number of items it can fit is 3. The first added item is recycled and cleared every time it exceeds 3. Recycling before clearing improved memory issues by a vast margin. You can now scroll up to 20 items and still not get a memory exception. I opted for LinkedHashmap over regular Hashmap because of the link as it would enable us to find the first entry in the map even though regular Hash-maps consumed less memory.
public void removeFirstEntryFromHashMap() {
if(map.size() > 3) {
Map.Entry<String,Drawable> entry = map.entrySet().iterator().next();
String key= entry.getKey();
Drawable drawable = entry.getValue();
((BitmapDrawable)drawable).getBitmap().recycle();
map.remove(key);
System.gc();
}
}
  • Removed API Call in Fragment: Getting Artwork details on every swipe caused the UI to freeze for seconds before views were set. Both Room and Retrofit run on background threads, so it was difficult to tell what was causing the screen to hang when that API call was made. Commenting it out seemed to fix this. Its effect on memory was also hard to gauge, but it was visible on the UI.
  • Gaussian Blur: I suspected the blur feature played a part in the memory overload. Scaling down the huge image to about a ratio, usually — a quarter (0.25), of the original size before blurring had a slightly positive impact on memory. Users wouldn't notice this because it was blurred too.
@WorkerThread
public Bitmap scaleDown(Bitmap input) {
int width = Math.round(ratio * input.getWidth());
int height = Math.round(ratio * input.getHeight());

return Bitmap.createScaledBitmap(input, width, height, true);
}

Post Implementation / Conclusion

Adding metrics to check which of the above solutions was what I should have done. I should have created a dummy project and watched what happened outside the main project if I had more time. We live and learn, though.

I still think some fragments are leaking, though not causing a memory usage spike, and I would still need a code review to fix that and other issues.

The average memory consumption now hovers between 250MB and 300MB after 15–20 swipes.

--

--

Fitzgerald Afful

Book reviews, flash fiction and random rants about iOS Eng. Portfolio: fitzafful.github.io