How To Detect & Fix Memory Leaks Using LeakCanary In Android

August 26, 2017

LeakCanary

The easiest way is to use LeakCanary.

Edit your application module’s build.gradle to add the following dependencies.

dependencies {
    ...
    // https://github.com/square/leakcanary/releases
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.2'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.2'
}

Setup LeakCanary at your Application class.

public class MainApplication extends Application {
    @Override 
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        refWatcher = LeakCanary.install(this);
        // Normal app init code...
    }

    public static RefWatcher getRefWatcher(Context context) {
        MainApplication application = (MainApplication) context.getApplicationContext();
        return application.refWatcher;
    }

    private RefWatcher refWatcher;
}

Edit AndroidManifest.xml to register you Application class.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="...">

    <application
        android:name=".MainApplication"
        ...>
        <activity ...>
          ...
        </activity>
        ...
    </application>
</manifest>

LeakCanary automatically detect memory leaks for all activites, thus no extra code is necessary. For fragment, it’s recommended add RefWatcher.

public class BlankFragment extends Fragment {
    ...

    @Override
    public void onDestroy() {
        super.onDestroy();
        MainApplication.getRefWatcher(getActivity()).watch(this);
    }
}

Nothing happens?

You run your app and start clicking on it to trigger actions which might cause memory leaks. You notice nothing happens thus assume there is no memory leaks, which might not be true.

I believe LeakCanary runs its analysis on some background process, where it could take a while for the notification for detected memory leaks to show up.

You can check is LeakCanary is running or not by looking at the logs.

... D/LeakCanary: Removing 1 heap dumps
... D/LeakCanary: Could not dump heap, previous analysis still is in progress.
...

I realize sometimes the anaysis process is not running when my app is running. If I close my app by clicking back to return to the home screen, then the above logs start appearing. Sometimes it could take up to a few minutes for the analysis to complete and show a memory leak notification on your phone.

You will notice LeakCanary will install an app named Leaks (you should find it next to your app’s icon). When you click on the notification of memory leak, it will open up the Leaks app and show you the detail. Sometimes the detail is not sufficient, thus you can check your logs.

... D/LeakCanary: In com.luasoftware.travelopy:1.6.6:75.
... D/LeakCanary: * com.luasoftware.travelopy.MomentListFragment has leaked:
... D/LeakCanary: * GC ROOT android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper.mParentInputMethodManager
... D/LeakCanary: * references android.view.inputmethod.InputMethodManager.mNextServedView
... D/LeakCanary: * references android.support.v7.widget.Toolbar.mContext
... D/LeakCanary: * references android.view.ContextThemeWrapper.mBase
... D/LeakCanary: * references com.luasoftware.travelopy.TabActivity.sectionsPagerAdapter
...
... D/LeakCanary: * leaks com.luasoftware.travelopy.MomentListFragment instance
... D/LeakCanary: * Retaining: 1.6 MB.
...
... D/LeakCanary: * Details:
... D/LeakCanary: * Instance of android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper
... D/LeakCanary: |   static $classOverhead = byte[1080]@1902886561 (0x716bbea1)
...
... D/LeakCanary: * Instance of android.view.inputmethod.InputMethodManager
... D/LeakCanary: |   static DEBUG_SIMPLE_LOG = false
...
... D/LeakCanary: * Instance of android.support.v7.widget.Toolbar
... D/LeakCanary: |   static TAG = java.lang.String@1897118232 (0x7113ba18)
...
... D/LeakCanary: * Excluded Refs:
... D/LeakCanary: | Field: android.view.textservice.SpellCheckerSession$1.this$0
... D/LeakCanary: | Field: android.view.Choreographer$FrameDisplayEventReceiver.mMessageQueue (always)
... D/LeakCanary: | Thread:FinalizerWatchdogDaemon (always)

Where is the leak and how to fix it?

Frankly, there is no straight forward answer for this.

If you are luckly, the logs will point you to the line of your code which shall give you a hint on which object had leaked. If you have a basic understanding on how memory leak occurs, you should be able to pinpoint the culprit quite easily. Common cases for Android are:

  • Object within your activity or fragment outlived the lifecyle of the activity, probably due to usage of static objects or background process or callbacks.
  • Avoid using non-static inner classes in an activity if instances of the inner class could outlive the activity’s lifecycle.
  • Passing activity context to external classes or static instance. For non-UI related request, it’s safer to use application’s context.
  • Observer which outlive the activity holding references to the activity.
  • Database cursor which is not closed.
  • Forget to unregister listener.

Use WeakReference whenever possible or necessary to reduce risk of memory leaks.

Sometimes the leaks are caused by the library you used, or by Android SDK itself.

Below are some memory leaks example encountered by me.

InputMethodManager memory leaks

One of the most common leak by Android SDK is by InputMethodManager, where the Android OS still hold a focus reference to an input even though the activity is closed.

LeakCanary tried to add LEAK CAN BE IGNORED message to such leaks, but it doesn’t seems to include all scenarios.

There is nothing much can be done but to ignore this leak.

References:

LocationServices memory leaks

There are some reported cases where LocationServices.FusedLocationApi.requestLocationUpdates is causing memory leaks when it had reference to the activity or fragment.

... D/LeakCanary: In com.luasoftware.travelopy:1.6.4:73.
... D/LeakCanary: * com.luasoftware.travelopy.MomentV2Activity has leaked:
... D/LeakCanary: * GC ROOT com.google.android.gms.internal.zzary$zzb.zzaGN
... D/LeakCanary: * references com.google.android.gms.internal.zzary$1.zzbkw (anonymous subclass of com.google.android.gms.internal.zzary$zza)
... D/LeakCanary: * leaks com.luasoftware.travelopy.MomentV2Activity instance
...  D/LeakCanary: * Retaining: 53 KB.
...
...  D/LeakCanary: * Details:
...  D/LeakCanary: * Instance of com.google.android.gms.internal.zzary$zzb
...  D/LeakCanary: |   static $classOverhead = byte[744]@319839233 (0x13105c01)
...  D/LeakCanary: |   zzaGN = com.google.android.gms.internal.zzary$1@322438656 (0x13380600)
...

WeakReference should be used to refer to activity or fragment when implementing LocationListener.

public static class SafeLocationListener implements LocationListener {
    private final WeakReference<MainActivity> ref;

    public SafeLocationListener(MainActivity instance) {
        this.ref = new WeakReference(instance);
    }


    @Override
    public void onLocationChanged(Location location) {
        MainActivity instance = ref.get();
        if (instance != null) {
            ...
        }
    }
}

protected void startLocationUpdates() {
    locationListener = new SafeLocationListener(this);
    LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, locationListener);
}

protected void stopLocationUpdates() {
    if (googleApiClient != null && googleApiClient.isConnected()) {
        if (locationListener != null) {
            LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, locationListener);
            locationListener = null;
        }
    }
}

References:

AppInvite memory leaks

There is a AppInvite memory leak issue with older version of Google Play. This could be resolved by using the latest version of Google Play.

... D/LeakCanary: In com.luasoftware.travelopy:1.6.4:73.
... D/LeakCanary: * com.luasoftware.travelopy.TabActivity has leaked:
... D/LeakCanary: * GC ROOT com.google.android.gms.internal.zzum$zze$1.zzaip (anonymous subclass of com.google.android.gms.internal.zzum$zza)
... D/LeakCanary: * references com.google.android.gms.internal.zzum$zze.zzaim
...

Old code.

boolean autoLaunchDeepLink = true;
AppInvite.AppInviteApi.getInvitation(googleApiClient, this.getActivity(), autoLaunchDeepLink)
        .setResultCallback(
                new ResultCallback<AppInviteInvitationResult>() {
                    @Override
                    public void onResult(AppInviteInvitationResult result) {
                        Log.d(TAG, "getInvitation:onResult:" + result.getStatus());
                        // Because autoLaunchDeepLink = true we don't have to do anything
                        // here, but we could set that to false and manually choose
                        // an Activity to launch to handle the deep link here.
                    }
                });

New code.

final WeakReference<Activity> activityRef = new WeakReference<Activity>(getActivity());
        FirebaseDynamicLinks.getInstance().getDynamicLink(activityRef.get().getIntent())
                .addOnSuccessListener(getActivity(), new OnSuccessListener<PendingDynamicLinkData>() {
                    @Override
                    public void onSuccess(PendingDynamicLinkData data) {
                        if (data == null) {
                            Log.d(TAG, "getInvitation: no data");
                            return;
                        }

                        // Get the deep link
                        Uri deepLink = data.getLink();

                        // Extract invite
                        FirebaseAppInvite invite = FirebaseAppInvite.getInvitation(data);
                        if (invite != null) {
                            String invitationId = invite.getInvitationId();
                        }

                        Log.d(TAG, "deepLink:" + deepLink);
                        if (deepLink != null) {
                            Intent intent = new Intent(Intent.ACTION_VIEW);
                            intent.setPackage(activityRef.get().getPackageName());
                            intent.setData(deepLink);

                            startActivity(intent);
                        }
                    }
                })
                .addOnFailureListener(getActivity(), new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        Log.w(TAG, "getDynamicLink:onFailure", e);
                    }
                });

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.