Android app will eat its entire memory... by design

Tags: android, java, memory, fragmentmanager

If you're developing for Android, I'm sure you already know that it's not as easy as it looks or people say. Well, OK, maybe it is EASY, but at the same time, it can be extremely FRUSTRATING. I mocked Android in one of my earlier posts. For a moment, I didn't need to go back to it and it was a sweet, sweet period in my life, but unfortunately, nothing good lasts forever.

This time, the client was right

The client I'm creating an Android app for claimed, that after a short while of using the app on some Samsung BigResolutionModel (I think it was S7 or something), the app suddenly crashes. On other phones, it's more or less OK.

Well, since it "worked on my phone"®, I didn't actually believe it, so I decided to visit the client and see it for myself.

Turned out, it was one of the very few moments when the client was right. There was something wrong with the app as it crashed after a while of using.

I attached the debugger to the phone, run the app, waited for a while and BOOM, there it was: OutOfMemoryError exception. OOM is never a good sign in Java.

After a little investigation, it turned out, that it was related to the FragmentManager class and navigation mechanism in the app. But let's start from the beginning.

High resolution = high memory usage

See, I'm using a navigation drawer menu that allows the user to navigate to different views. The views are being represented as Fragments that are being switched when a user selects an item in the navigation drawer menu. That's pretty basic stuff.

The problem was, that one of the views contained 9 MPAndroid charts. Each chart is portrait-screen-wide, so it's about 1400px wide on S7 screen. Looking at the memory profiler it turned out, that each chart used about 5.5 MB of memory, so in total, the app uses about 50 MB of RAM just for charts. This is a lot for an Android app (not a game). Nevertheless, if each time user navigates to another view, the memory would be properly freed, then everything would be OK. But unfortunately, it wasn't.

Every time user clicked in the menu item, the FragmentManager loaded proper Fragment, something like this:

// called when user navigates in the navigation drawer
// item is just enum of clicked item
public void onNavigationDrawerItemSelected( NavigationDrawerFragment.DrawerItem item ) {
    FragmentManager fragmentManager = getSupportFragmentManager();
    Fragment newFragment = ...; // set appropriate fragment based on NavigationDrawerFragment.DrawerItem item
    fragmentManager
        .beginTransaction()
        .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right)
        .replace(R.id.container, newFragment)
        .addToBackStack()  // note, that we are adding fragment to the "back stack"
        .commit();
}

Each Fragment was being added to back stack so that when the user pushed "back" button, he/she was going back to the previous view.

The problems

Now, there are two problems with the code above and, partially, with Android's FragmentManager design:

  1. as the user navigates more and more, the back stack grows on and on, and
  2. when the user was at this highly-charted view (remember, 9x5.5 = 50 MB of RAM), there was a possibility that this view gets "reloaded" i.e. navigated to exactly the same view. So this view was being added to the back stack again. And again. And again. And for some reason, it was eating memory more when navigating to another view and returning to this view. What's worse, the memory could not be GC'ed as it was always in use because of residing in back stack.

The number 2 was the direct reason why I got the OOM exception: after 3-4 of view self-reloads, the memory peaked at 256 MB. The solution to this problem was not to add the same view again or just to pop the same view from the stack, before adding it again. This helped much as the (now unused) memory could be safely freed by GC:

// called when user navigates in the navigation drawer
// item is just enum of clicked item
public void onNavigationDrawerItemSelected( NavigationDrawerFragment.DrawerItem item, boolean removeFromStackBeforeAdd  ) {
    FragmentManager fragmentManager = getSupportFragmentManager();
    Fragment newFragment = ...; // set appropriate fragment based on NavigationDrawerFragment.DrawerItem item
    if(removeFromStackBeforeAdd)
        fragmentManager.popBackStackImmediate();
    fragmentManager
        .beginTransaction()
        .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right)
        .replace(R.id.container, newFragment)
        .addToBackStack()
        .commit();
    System.gc(); // now the memory we poped back above could be freed
}

But there was still problem number 1: when doing some navigation stress-tests (navigating app randomly like a monkey), the ever increasing back stack was still eating memory, byte by byte and there was no way of removing oldest items from the stack (I know, I know, if one could do that, it won't be a "stack" anymore). I asked at SO about it but there was no satisfactory answer.

The solution (or workaround)

So in the act of desperation, I threw away .addToBackStack() and created my 'back stack' implementation that just remembers which menu items user clicked:

// called when user navigates in the navigation drawer
// item is just enum of clicked item
public void onNavigationDrawerItemSelected( NavigationDrawerFragment.DrawerItem item, boolean addToNavigationPath  ) {
    FragmentManager fragmentManager = getSupportFragmentManager();
    Fragment newFragment = ...; // set appropriate fragment based on NavigationDrawerFragment.DrawerItem item
    if(addToNavigationPath)
        _navigationPathManager.pushPathItem( item ); // _navigationPathManager is my "path manager"
    fragmentManager
        .beginTransaction()
        .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right)
        .replace(R.id.container, newFragment)
        .commit(); // no adding to back stack
}

The pushPathItem() additionally checks if there're too many items and just removes the oldest one since I'm sure the user won't remember where he/she was 20 or 30 clicks ago.

I also had to change the onBackPressed() method to make use of my "path manager":

@Override
public void onBackPressed() {
    NavigationDrawerFragment.DrawerItem prevPathItem = _navigationPathManager.popPathItemPeekPrev();
    if( prevPathItem != null ){
        // go to item
        onNavigationDrawerItemSelected(prevPathItem, false );
        // check/highlight it in the menu
        _navigationDrawerFragment.checkItem(prevPathItem);
    }
}

This approach allows my app to keep the memory usage under constraints. The entire implementation of NavigationPathManager is here:

And so, there it is: a solution, or rather a workaround, for one of the many Android quirks.

After all those struggles, the conclusion I came up with was:

Read also

The wonders of Android development

Years have passed, Android now is in version 6.0 or 7.0, but some things just don't change. There are still few very and annoying bugs in Android that I stumbled upon.

Automatic watch accuracy check

Automatic watch may not keep the perfect timing, but it's cool and, contrary to quartz watch, have a soul. Plus there's a chance it will continue to work even after you're long gone.

3 essential tools/apps for devs or IT

Here's my list of 3 favorite tools or apps for every IT guy or developer, regardless of technology you are breathing with.

Comments