0

Using LeakCanary, it says I have a memory leak when exiting the activity that uses MapBox to display a map. Heap info and code below. I don't see anything, but then, I don't know what I'm looking for. Can you see the cause?

In com.myapp.debug:1.0:1. * com.myapp.DirectionsActivity has leaked: * GC ROOT static android.app.ActivityManager.mContext * leaks com.myapp.DirectionsActivity instance

  • Reference Key: b0fc445c-fe16-46e8-aff4-71acd3924d52
  • Device: samsung samsung SM-T530NU matissewifiue
  • Android Version: 5.0.2 API: 21 LeakCanary: 1.3.1
  • Durations: watch=5272ms, gc=189ms, heap dump=6599ms, analysis=75048ms

  • Details:

  • Class android.app.ActivityManager | static $staticOverhead = byte[] [id=0x70bc2699;length=504;size=520] | static AMS_POLICY_ENFORCING = java.lang.String [id=0x701edfb0] | static META_HOME_ALTERNATE = java.lang.String [id=0x70584898] | static TAG = java.lang.String [id=0x704a96a0] | static mContext = com.myapp.DirectionsActivity [id=0x13061da0] | static BROADCAST_STICKY_CANT_HAVE_PERMISSION = -1 | static BROADCAST_SUCCESS = 0 | static COMPAT_MODE_ALWAYS = -1 | static COMPAT_MODE_DISABLED = 0 | static COMPAT_MODE_ENABLED = 1 | static COMPAT_MODE_NEVER = -2 | static COMPAT_MODE_TOGGLE = 2 | static COMPAT_MODE_UNKNOWN = -3 | static INTENT_SENDER_ACTIVITY = 2 |
    static INTENT_SENDER_ACTIVITY_RESULT = 3 | static INTENT_SENDER_BROADCAST = 1 | static INTENT_SENDER_SERVICE = 4 |
    static MOVE_TASK_NO_USER_ACTION = 2 | static MOVE_TASK_WITH_HOME = 1 | static PROCESS_STATE_BACKUP = 5 | static PROCESS_STATE_CACHED_ACTIVITY = 11 | static PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 12 | static PROCESS_STATE_CACHED_EMPTY = 13 | static PROCESS_STATE_HEAVY_WEIGHT = 6 | static PROCESS_STATE_HOME = 9 | static PROCESS_STATE_IMPORTANT_BACKGROUND = 4 | static PROCESS_STATE_IMPORTANT_FOREGROUND = 3 | static PROCESS_STATE_LAST_ACTIVITY = 10 | static PROCESS_STATE_PERSISTENT = 0 | static PROCESS_STATE_PERSISTENT_UI = 1 | static PROCESS_STATE_RECEIVER = 8 | static PROCESS_STATE_SERVICE = 7 |
    static PROCESS_STATE_TOP = 2 | static RECENT_IGNORE_HOME_STACK_TASKS = 8 | static RECENT_IGNORE_UNAVAILABLE = 2 | static RECENT_INCLUDE_PROFILES = 4 | static RECENT_WITH_EXCLUDED = 1 |
    static REMOVE_ALL_RECENT_TASKS = 8 | static REMOVE_ALL_TASKS = 2 |
    static REMOVE_TASK_EXCEPT_RECENTS = 16 | static REMOVE_TASK_IMMEDIATELY = 4 | static REMOVE_TASK_KILL_PROCESS = 1 | static START_CANCELED = -6 | static START_CANCELED_BY_TEMPERATURE = -8 | static START_CLASS_NOT_FOUND = -2 | static START_DELIVERED_TO_TOP = 3 | static START_FLAG_DEBUG = 2 | static START_FLAG_ONLY_IF_NEEDED = 1 | static START_FLAG_OPENGL_TRACES = 4 | static START_FORWARD_AND_REQUEST_CONFLICT = -3 | static START_INTENT_NOT_RESOLVED = -1 | static START_NOT_ACTIVITY = -5 |
    static START_NOT_VOICE_COMPATIBLE = -7 | static START_PERMISSION_DENIED = -4 | static START_RETURN_INTENT_TO_CALLER = 1 | static START_RETURN_LOCK_TASK_MODE_VIOLATION = 5 | static START_SUCCESS = 0 | static START_SWITCHES_CANCELED = 4 | static START_TASK_TO_FRONT = 2 | static USER_OP_IS_CURRENT = -2 | static USER_OP_SUCCESS = 0 | static USER_OP_UNKNOWN_USER = -1 | static gMaxRecentTasks = -1 | static localLOGV = false
  • Instance of com.myapp.DirectionsActivity | TAG = java.lang.String [id=0x1347b5c0] | client = com.mapbox.services.directions.v5.MapboxDirections [id=0x1351f4c0] |
    currentRoute = com.mapbox.services.directions.v5.models.DirectionsRoute [id=0x136c94e0] | map = com.mapbox.mapboxsdk.maps.MapboxMap [id=0x12c94600] | mapView = com.mapbox.mapboxsdk.maps.MapView [id=0x13422400] | mDelegate = android.support.v7.app.AppCompatDelegateImplV14 [id=0x12c30fc0] |
    mResources = null | mEatKeyUpEvent = false | mThemeId = 2131361951 | mFragments = android.support.v4.app.FragmentController [id=0x13473ac0] | mHandler = android.support.v4.app.FragmentActivity$1 [id=0x1347b5a0] |
    mMediaController = null | mPendingFragmentActivityResults = android.support.v4.util.SparseArrayCompat [id=0x13482080] | mCreated = true | mNextCandidateRequestIndex = 0 | mOptionsMenuInvalidated = false | mReallyStopped = true | mRequestedPermissionsFromFragment = false | mResumed = false |
    mRetaining = false | mStopped = true |
    mStartedActivityFromFragment = false |
    mStartedIntentSenderFromFragment = false | mActionBar = null |
    mActivityInfo = android.content.pm.ActivityInfo [id=0x12f34340] |
    mActivityTransitionState = android.app.ActivityTransitionState [id=0x1347a280] | mAllLoaderManagers = android.util.ArrayMap [id=0x134b4d80] | mApplication = android.app.Application [id=0x12c95a80] | mComponent = android.content.ComponentName [id=0x12c920b0] | mContainer = android.app.Activity$1 [id=0x13473a90] | mCurrentConfig = android.content.res.Configuration [id=0x13475a20] | mDecor = null | mDefaultKeySsb = null |
    mEmbeddedID = null | mEnterTransitionListener = android.app.SharedElementCallback$1 [id=0x724871f0] |
    mExitTransitionListener = android.app.SharedElementCallback$1 [id=0x724871f0] | mFeatureContextMenuListener = android.app.Activity$FeatureContextMenuListener [id=0x13473a60] |
    mFragments = android.app.FragmentManagerImpl [id=0x134757f0] |
    mHandler = android.os.Handler [id=0x1347b580] | mInjectionManager = null | mInstanceTracker = android.os.StrictMode$InstanceTracker [id=0x13473aa0] | mInstrumentation = android.app.Instrumentation [id=0x12c2cd90] | mIntent = android.content.Intent [id=0x133fb390] | mLastNonConfigurationInstances = null | mLauncherBooster = null |
    mLoaderManager = null | mMainThread = android.app.ActivityThread [id=0x12c42100] | mManagedCursors = java.util.ArrayList [id=0x1347b540] | mManagedDialogs = null | mMenuInflater = null | mMultiWindowStyle = com.samsung.android.multiwindow.MultiWindowStyle [id=0x13478790] | mParent = null | mResultData = null |
    mSearchManager = null | mSubDecor = null | mSubWindow = null |
    mSubWindowDummpy = null | mTitle = java.lang.String [id=0x134a3dc0] | mToken = android.os.BinderProxy [id=0x12c82fa0] |
    mTranslucentCallback = null | mUiThread = java.lang.Thread [id=0x8736cfb0] | mVoiceInteractor = null | mWindow = com.android.internal.policy.impl.PhoneWindow [id=0x12db0110] |
    mWindowManager = android.view.WindowManagerImpl [id=0x1347ba00] |
    myName = java.lang.String [id=0x1347b260] | DEBUG_ELASTIC = false | isElasticEnabled = false | mCalled = true |
    mChangeCanvasToTranslucent = false | mChangingConfigurations = false | mCheckedForLoaderManager = true | mConfigChangeFlags = 0 |
    mDefaultKeyMode = 0 | mDestroyed = true | mDoReportFullyDrawn = false | mEnableDefaultActionBarUp = false | mFinished = true |
    mFlipfont = 0 | mIdent = 254734993 | mLoadersStarted = false |
    mPreventEmbeddedTabs = false | mResultCode = 0 | mResumed = false | mStackedHeight = -1 | mStartedActivity = false | mStopped = true | mSubWindowAdded = false | mTemporaryPause = false |
    mTitleColor = 0 | mTitleReady = true | mVisibleBehind = false |
    mVisibleFromClient = true | mVisibleFromServer = true |
    mWindowAdded = true | mInflater = com.android.internal.policy.impl.PhoneLayoutInflater [id=0x134789a0] | mOverrideConfiguration = null | mResources = android.content.res.Resources [id=0x12c24700] | mTheme = android.content.res.Resources$Theme [id=0x1347ba20] | mThemeResource = 2131361951 | mBase = android.app.ContextImpl [id=0x12e06b20]
public class DirectionsActivity extends AppCompatActivity {

    private final String TAG = "DirectionsActivity";

    private MapView mapView;
    private MapboxMap map;
    private DirectionsRoute currentRoute;

    private MapboxDirections client;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // employee location is destination
        Intent intent = getIntent();
        double employeeLatitude = intent.getDoubleExtra("latitude", new Double(getString(R.string.state_capital_latitude)));
        double employeeLongitude = intent.getDoubleExtra("longitude", new Double(getString(R.string.state_capital_longitude)));

        // Mapbox access token is configured here. This needs to be called either in your application
        // object or in the same activity which contains the mapview.
        MapboxAccountManager.start(this, getString(R.string.access_token));

        // This contains the MapView in XML and needs to be called after the account manager
        setContentView(R.layout.activity_mas_directions);

        // get current location from preferences for use as origin
        Location currentLocation = PreferencesUtilities.getCurrentLocation(this.getApplicationContext());
        final Position origin = Position.fromCoordinates(currentLocation.getLongitude(), currentLocation.getLatitude());

        final Position destination = Position.fromCoordinates(employeeLongitude, employeeLatitude); // yes, long,lat - strange

        this.setTitle("Origin: (" + origin.getLatitude() + ", " + origin.getLongitude() + ")  >>  Destination: (" + destination.getLatitude() + ", " + destination.getLongitude() + ")" );

        // Create Icon objects for the marker to use
        IconFactory iconFactory = IconFactory.getInstance(this);
        Drawable iconDrawable = ContextCompat.getDrawable(this, R.drawable.green_pin); // pin png is 125x125
        final Icon greenPinIcon = iconFactory.fromDrawable(iconDrawable);
        iconDrawable = ContextCompat.getDrawable(this, R.drawable.red_pin); // pin png is 125x125
        final Icon redPinIcon = iconFactory.fromDrawable(iconDrawable);

        // Setup the MapView
        mapView = (MapView) findViewById(R.id.mapView);
        mapView.onCreate(savedInstanceState);
        mapView.getMapAsync(new OnMapReadyCallback() {
            @Override
            public void onMapReady(MapboxMap mapboxMap) {
                map = mapboxMap;

                // Add origin and destination to the map
                LatLng originLatLng = (new LatLng(origin.getLatitude(), origin.getLongitude()));
                mapboxMap.addMarker(new MarkerViewOptions()
                        .position(originLatLng)
                        .anchor((float)0.5, (float)1.0) // bottom, middle I think
                        //.anchor(1, (float)0.5) // (0,0) is top left
                        .title("Origin")
                        .snippet("current location: (" + origin.getLatitude() + ", " + origin.getLongitude() + ")")
                        .icon(greenPinIcon));

                LatLng destinationLatLng = (new LatLng(destination.getLatitude(), destination.getLongitude()));
                mapboxMap.addMarker(new MarkerViewOptions()
                        .position(destinationLatLng)
                        .anchor((float)0.5, (float)1.0) // bottom, middle I think
                        //.anchor(1, (float)0.5) // (0,0) is top left
                        .title("Destination")
                        .snippet("destination: (" + destination.getLatitude() + ", " + destination.getLongitude() + ")")
                        .icon(redPinIcon));

                LatLngBounds latLngBounds = new LatLngBounds.Builder()
                        .include(originLatLng) // Northeast
                        .include(destinationLatLng) // Southwest
                        .build();

                mapboxMap.easeCamera(CameraUpdateFactory.newLatLngBounds(latLngBounds, 50), 5000);

                // Get route from API
                try {
                    getRoute(origin, destination);
                }
                catch (ServicesException servicesException) {
                    Log.e(TAG, servicesException.toString());
                    servicesException.printStackTrace();
                }
            }
        });
    }

    private void getRoute(Position origin, Position destination) throws ServicesException {

        client = new MapboxDirections.Builder()
                .setOrigin(origin)
                .setDestination(destination)
                .setProfile(DirectionsCriteria.PROFILE_CYCLING)
                .setAccessToken(MapboxAccountManager.getInstance().getAccessToken())
                .build();

        client.enqueueCall(new Callback<DirectionsResponse>() {
            @Override
            public void onResponse(Call<DirectionsResponse> call, Response<DirectionsResponse> response) {
                // You can get the generic HTTP info about the response
                //Log.d(TAG, "Response code: " + response.code());
                if (response.body() == null) {
                    Log.e(TAG, "No routes found, make sure you set the right user and access token.");
                    return;
                } else if (response.body().getRoutes().size() < 1) {
                    Log.e(TAG, "No routes found");
                    return;
                }

                // Print some info about the route
                currentRoute = response.body().getRoutes().get(0);
                //Log.d(TAG, "Distance: " + currentRoute.getDistance());
                Double km = currentRoute.getDistance() / 1000;
                // there are 4 digits to the right of the decimal, make it 2
                String kilometers = km.toString();
                int index = kilometers.lastIndexOf(".");
                kilometers = kilometers.substring(0, index + 3);
                Toast.makeText(
                        DirectionsActivity.this,
                        "Route is " + kilometers + " kilometers",
                        Toast.LENGTH_SHORT).show();

                // Draw the route on the map
                drawRoute(currentRoute);
            }

            @Override
            public void onFailure(Call<DirectionsResponse> call, Throwable throwable) {
                Log.e(TAG, "Error: " + throwable.getMessage());
                Toast.makeText(DirectionsActivity.this, "Error: " + throwable.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });
    }

    private void drawRoute(DirectionsRoute route) {
        // Convert LineString coordinates into LatLng[]
        LineString lineString = LineString.fromPolyline(route.getGeometry(), Constants.OSRM_PRECISION_V5);
        List<Position> coordinates = lineString.getCoordinates();
        LatLng[] points = new LatLng[coordinates.size()];
        for (int i = 0; i < coordinates.size(); i++) {
            points[i] = new LatLng(
                    coordinates.get(i).getLatitude(),
                    coordinates.get(i).getLongitude());
        }

        // Draw Points on MapView
        map.addPolyline(new PolylineOptions()
                .add(points)
                .color(Color.parseColor("#009688"))
                .width(5));
    }

    @Override
    public void onResume() {
        super.onResume();
        mapView.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
        mapView.onPause();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        mapView.onSaveInstanceState(outState);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // Cancel the directions API request
        if (client != null) {
            client.cancelCall();
        }
        mapView.onDestroy();
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        mapView.onLowMemory();
    }
}

Edit:
Adding info in response to @cammace. This is how I get the user's location. The leak occurs while the application is in the foreground (and had never been in the background).

MainActivity

private Observable<Location> locationUpdateObservable;
private Subscription locationUpdateSubscription;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // Location Observable
    final LocationRequest locationRequest = LocationRequest.create()
            //.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
            .setInterval(30000); // milliseconds
    // net.kjulio.rxlocation
    locationUpdateObservable = RxLocation.locationUpdates(mActivity, locationRequest);
    locationUpdateSubscription = locationUpdateObservable.subscribe(new Action1<Location>() {
        @Override
        public void call(Location location) {
            // save current location to preferences
            PreferencesUtilities.setCurrentLocation(mActivity.getApplicationContext(), location);
        }
    }, new Action1<Throwable>() {
        @Override
        public void call(Throwable throwable) {
            Toast.makeText(mActivity, "Can't get location Throwable: " + throwable.toString(),  Toast.LENGTH_SHORT).show();
        }
    });
Al Lelopath
  • 6,448
  • 13
  • 82
  • 139

1 Answers1

0

How exactly are you getting the users location? This can cause memory leaks if you aren't removing location request when your application isn't in the foreground. Also for the directions API, you should cancel your request in the rare case that a request is made but the user exits the app before the API response happens.

cammace
  • 3,138
  • 1
  • 13
  • 18