Debugging Memory Leaks In An Android App

  • ·

Debugging memory leaks in an android app can be a bit tricky. Recently we had to deal with this issue in one of our apps. This was caused due to a recent refactoring we did in our code base. We were holding on to an activity instance that needed to be Garbage Collected**(GC’d)** after the activity was destroyed. Yes, you would think the Android Runtime**(ART)** will GC this instance but no. There was another living object in the application that was still holding on to the activity instance so it couldn’t be GC’d.

Investigating Memory Leaks

Finding out all this information just by reading the codebase can be difficult and time-consuming. Luckily there are monitoring tools out there that help in reporting and investigating memory leaks. Android Studio comes with a Memory Monitor that essentially monitors memory activities by your app. You can then dump the Java Heap and analyze it for optimizations and possible memory leaks. There is also LeakCanary by the folks at Square. It allows you to integrate memory monitoring directly into your app so it reports any memory leaks in the app.

I’m going to share with you the strategy we used in detecting the memory leak in our app, the problematic code, and the refactoring we did to eliminate the issue.

1. Used LeakCanary To Detect Memory Leaks In the App

In our app’s devel flavor, we have LeakCanary enabled. This is a practice we do with all of our apps as it makes it easier to detect and fix memory leaks as we develop. With our devel build variant launched in an emulator, we used it like a regular user will use the app. We fetched the needed content, scroll through them and nothing really happened. The issue showed up just when we rotated the emulator to change screen orientation, after a few seconds, LeakCanary reported a memory leak, followed by a dump of our app’s heap. LeakCanary displays the heap in an optimized way so it’s easier to find which dominant object is still referencing an object that needs to be GC’d.

2. Enabled Profiling To Monitor Memory Activities

Once we have detected a memory leak in our app, we enabled Memory Monitor to see the allocation and deallocations of memories in our apps. Also, we forced excessive GC to see which objects are GC’d or not.

3. Dumped Java Heap And Analyzed The Trace.

After we had run the profiler for a while, we took a heap dump of it. Using the inbuilt HPROF Viewer in Android studio, we’re able to analyze the trace for all possible memory leaks in the app. Indeed after analyzing it, we had one issue of memory leak. We were able to jump into the source code to see which code is causing the issue.

4. Located Defected Code

This leaks an Activity.

public class Launcher {
    // This will be strongly held by the instance of the Launcher class.
    private Activity mActivity;

    @Inject
    public Launcher(Activity activity) {
        mActivity = activity;
    }

    public void launchReviewList(Context context, Long movieId) {
        context.startActivity(ListReviewsActivity.getIntent(context, movieId));
    }

    // This is bad code
    public void launchMovieDetails(FragmentActivity activity, Long movieId, View view) {
        ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                // The context of the activity
                activity,
                Pair.create(view, activity.getString(R.string.transition_movie_details)),
                Pair.create(view, activity.getString(R.string.transition_movie_details_background))
        );
        ActivityCompat.startActivity(activity, DetailMovieActivity.getIntent(mActivity, movieId),
                options.toBundle());
    }

    public void launchReminders(Context context) {
        context.startActivity(ListRemindersActivity.getIntent(context));
    }
}

The dominant object, mLauncher was holding onto the instance of the activity class.


public class ListMovieFragment extends BaseRecyclerViewFragment<MovieModel, MovieAdapter> implements
        MovieListView, RecyclerViewItemTouchListenerAdapter.RecyclerViewOnItemClickListener,
        OnLoadMoreListener, SwipeRefreshLayout.OnRefreshListener {

    @BindView(R.id.loading_list)
    ProgressBar mProgressBar;

    @BindView(android.R.id.empty)
    View mEmptyView;

    @Inject
    ListMoviePresenter mListMoviePresenter;

    @Inject
    Launcher mLauncher;

    private boolean mIsPaginating;

    private boolean isFromInternet;

    public ListMovieFragment() {
        super(MovieAdapter.class, R.layout.fragment_movie_list, 0);
    }
    ...
}

An instance of Launcher class, mLauncher is declared in the ListMovieFragment class and it’s referencing an instance of the MainActivity which is declared in the Launcher class as mActivity. Because the mLauncher declared inside ListMovieFragment class still exists even after it has been destroyed by the activity hosting it as the hosting activity is destroyed.

5. Refactored Defected Code To Eliminate The Issue


public class Launcher {

    @Inject
    public Launcher() {
    }

    public void launchReviewList(Context context, Long movieId) {
        context.startActivity(ListReviewsActivity.getIntent(context, movieId));
    }

    public void launchMovieDetails(Activity activity, Long movieId, View view) {
        ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                // The context of the activity
                activity,
                Pair.create(view, activity.getString(R.string.transition_movie_details)),
                Pair.create(view, activity.getString(R.string.transition_movie_details_background))
        );
        ActivityCompat.startActivity(activity, DetailMovieActivity.getIntent(activity, movieId),
                options.toBundle());
    }

    public void launchReminders(Context context) {
        context.startActivity(ListRemindersActivity.getIntent(context));
    }
}

We removed the global instance of Activity, mActivity, and instead passed it as a parameter to the method that needed it. This way it can be GC'd easily as there is no stronghold of it anymore.

Conclusion

If you’re curious about memory leaks in general and ways to avoid them, you can read these great articles below.

  1. Eight Ways Your Android App Can STOP Leaking memory
  2. Memory Monitor
  3. HPROF Viewer and Analyzer
  4. Investigating Your RAM Usage
  5. Detect all memory leaks with LeakCanary! - Youtube Link