Android Fragments on Backstack taking up too much memory

Max Worg picture Max Worg · Feb 12, 2015 · Viewed 11.8k times · Source

PROBLEM:

I have an Android application that allows a user to browse to a user's profile ViewProfileFragment. Inside ViewProfileFragment a user can click on an image that will take him to StoryViewFragment where various users' photos show up. It is possible to click on a user profile photo that will take them to another instance of ViewProfileFragment with the new user's profile. If a user repeatedly clicks on user's profiles, clicks an image that takes them to the gallery then clicks on another profile the Fragments stack up in memory quickly causing the dreaded OutOfMemoryError. Here is a diagram flow of what I am describing:

UserA clicks on Bob's profile. Inside Bob's profile UserA clicks on ImageA taking him to a gallery of photos of various users (including Bob's). UserA clicks on profile of Sue then on one of her images - process repeats, etc, etc.

UserA -> ViewProfileFragment
         StoryViewFragment -> ViewProfileFragment
                               StoryViewFragment -> ViewProfileFragment

So as you can see from a typical flow there are lots of instances of ViewProfileFragment and StoryViewFragment piling up in the backstack.

RELEVANT CODE

I am loading these in as fragments with the following logic:

//from MainActivity
fm = getSupportFragmentManager();
ft = fm.beginTransaction();
ft.replace(R.id.activity_main_content_fragment, fragment, title);
ft.addToBackStack(title);

WHAT I'VE TRIED

1) I am specifically using FragmentTransaction replace so that the onPause method will be triggered when the replace takes place. Inside onPause I am trying to free up as many resources as I can (such as clearing out data in ListView adapters, "nulling" out variables, etc) so that when the fragment is not the active fragment and pushed onto the backstack there will be more memory freed up. But my efforts to free up resources is only a partial success. According to MAT I still have a lot of memory that is consumed by GalleryFragment and ViewProfileFragment.

2) I've also removed the call to addToBackStack() but obviously that offers a poor user experience because they can't traverse back (the app just closes when the user hits the back button).

3) I have used MAT to find all of the objects that I take up a lot of space and I have dealt with those in various ways inside the onPause (and onResume) methods to free up resources but they are still considerable in size.

enter image description here

4) I also wrote a for loop in both fragments' onPause that sets all of my ImageViews to null using the following logic:

 for (int i=shell.getHeaderViewCount(); i<shell.getCount(); i++) {

     View h = shell.getChildAt(i);
     ImageView v = (ImageView) h.findViewById(R.id.galleryImage);
       if (v != null) {
           v.setImageBitmap(null);
       }
  }

myListViewAdapter.clear()

QUESTIONS

1) Am I overlooking a way to allow a Fragment to remain on the backstack but also free up its resources so that the cycle of .replace(fragment) doesn't eat up all of my memory?

2) What are the "best practices" when it is expected that a lot of Fragments could be loaded onto the backstack? How does a developer correctly deal with this scenario? (Or is the logic in my application inherently flawed and I'm just doing it wrong?)

Any help in brainstorming a solution to this would be greatly appreciated.

Answer

Martin Marconcini picture Martin Marconcini · Feb 16, 2015

It's hard to see the whole picture (even tho you have shown us a lot of information), without concrete access to your source code, which I'm sure it would be impractical if not impossible.

That being said, there are a few things to keep in mind when working with Fragments. First a piece of disclaimer.

When Fragments were introduced, they sounded like the best idea of all times. Being able to display more than one activity at the same time, kinda. That was the selling point.

So the whole world slowly started using Fragments. It was the new kid on the block. Everybody was using Fragments. If you were not using Fragments, chances were that "you were doing it wrong".

A few years and apps later, the trend is (thankfully) reverting back to more activity, less fragment. This is enforced by the new APIs (The ability to transition between activities without the user really noticing, as seen in the Transition APIs and such).

So, in summary: I hate fragments. I believe it's one of the worst Android implementations of all time, that only gained popularity because of the lack of Transition Framework (as it exists today) between activities. The lifecycle of a Fragment is, if anything, a ball of random callbacks that are never guaranteed to be called when you expect them.

(Ok, I am exaggerating a little bit, but ask any Android seasoned developer if he had trouble with Fragments at some point and the answer will be a resounding yes).

With all that being said, Fragments work. And so your solution should work.

So let's start looking at who/where can be keeping these hard references.

note: I'm just gonna toss ideas out here of how I would debug this, but I will not likely provide a direct solution. Use it as a reference.

WHAT IS GOING ON?: You're adding fragments to the Backstack. The backstack stores a hard reference to the Fragment, not weak or soft. (source)

Now who stores a backstack? FragmentManager and… as you guessed, it uses a hard live reference as well (source).

And finally, each activity contains a hard reference to the FragmentManager.

In short: until your activity dies, all the references to its fragments will exist in memory. Regardless of add/remove operations that happened at Fragment Manager level / backstack.

WHAT CAN YOU DO? A couple of things come to my mind.

  1. Try using a simple image loader/cache lib like Picasso, if anything to make sure that images are not being leaked. You can later remove it if you want to use your own implementation. For all its flaws, Picasso is really simple to use and has come to a state where it deals with memory "the right way".

  2. After you have removed the "I may be leaking bitmaps" problem out of the picture (no pun intended!), then it's time to revisit your Fragment lifecycle. When you put a fragment in the backstack, it's not destroyed, but… you have a chance to clear resources: Fragment#onDestroyView() is called. And here is where you want to make sure that the fragment nullifies any resources.

You do not mention if your fragments are using setRetainInstance(true), be careful with that, because these do not get destroyed/recreated when the Activity is destroyed/recreated (e.g.: rotation) and all the views may be leaked if not properly handled.

  1. Finally, but this is harder to diagnose, maybe you'd like to revisit your architecture. You're launching the same fragment (viewprofile) multiple times, you may want to consider instead, reusing the same instance and load the "new user" in it. Backstack could be handled by keeping track of a list of users in the order they are loaded, so you could intercept onBackPressed and move "down" the stack, but always loading the new/old data as the user navigates. The same goes for your StoryViewFragment.

All in all, these are all suggestions that came from my experience, but it's really hard to help you unless we can see more in detail.

Hopefully it proves to be a starting point.

Best of luck.