3

In one of the fragments in my app, I require location updates in order to determine when the user is near a short list of locations so relevant information is available to the user. When I create a location update request in the onConnected() callback using

LocationServices.FusedLocationApi.requestLocationUpdates(client, LocationRequest.create(), this);

I also remove the updates in the onPause() method using

LocationServices.FusedLocationApi.removeLocationUpdates(client, this);

However even though I call the method to remove location updates I get a warning from leak canary that my fragment is being leaked whenever I navigate away from that fragment (all fragment navigation is done using replace() transactions). Am I doing something wrong in initiating the location request in my fragment or am I not cleaning up the request properly in onPause(), any help with this would be greatly appreciated.

Here is the code from a simplified model of my fragment class

public class BlankFragment extends Fragment implements GoogleApiClient.ConnectionCallbacks
    , GoogleApiClient.OnConnectionFailedListener, LocationListener {

private GoogleApiClient client;
private TextView textView;

public BlankFragment() {
    // Required empty public constructor
}


@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View root = inflater.inflate(R.layout.fragment_blank, container, false);
    textView = (TextView) root.findViewById(R.id.location);
    client = new GoogleApiClient.Builder(getContext())
            .addApi(LocationServices.API)
            .addConnectionCallbacks(this)
            .addOnConnectionFailedListener(this)
            .build();
    return root;
}

@Override
public void onConnected(@Nullable Bundle bundle) {
    startLocationUpdates();
}

private void startLocationUpdates() {
    if (client.isConnected()) {
       LocationServices.FusedLocationApi.requestLocationUpdates(client, LocationRequest.create(), this);

    }
}

private void stopLocationUpdates(){
    LocationServices.FusedLocationApi.removeLocationUpdates(client, this);
}



@Override
public void onConnectionSuspended(int i) {

}

@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {

}

@Override
public void onLocationChanged(Location location) {
    textView.setText(location.toString());
}

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

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

@Override
public void onStart() {
    client.connect();
    super.onStart();
}

@Override
public void onStop() {
    if (client.isConnected()) {
    client.disconnect();
    }
    super.onStop();
}}

Also here is the generated leak trace

In com.example.testlocation:1.0:1.
* com.example.testlocation.BlankFragment has leaked:
* GC ROOT com.google.android.gms.internal.zzary$zzb.zzaGN
* references com.google.android.gms.internal.zzary$9.zzbkw (anonymous subclass of com.google.android.gms.internal.zzary$zza)
* leaks com.example.testlocation.BlankFragment instance
* Retaining: 7.1 KB.
* Reference Key: 720085d5-af68-42f3-a291-ef959e4c8ffa
* Device: Huawei google Nexus 6P angler
* Android Version: 7.1.2 API: 25 LeakCanary: 1.5 00f37f5
* Durations: watch=5031ms, gc=167ms, heap dump=1418ms, analysis=137993ms
* Details:
* Instance of com.google.android.gms.internal.zzary$zzb
|   static $classOverhead = byte[240]@316980881 (0x12e4be91)
|   zzaGN = com.google.android.gms.internal.zzary$9@316955016 (0x12e45988)
|   mDescriptor = java.lang.String@317419776 (0x12eb7100)
|   mObject = 483616889632
|   mOwner = com.google.android.gms.internal.zzary$zzb@317493408 (0x12ec90a0)
|   shadow$_klass_ = com.google.android.gms.internal.zzary$zzb
|   shadow$_monitor_ = 0
* Instance of com.google.android.gms.internal.zzary$9
|   static $classOverhead = byte[312]@317158785 (0x12e77581)
|   zzbkw = com.example.testlocation.BlankFragment@316908800 (0x12e3a500)
|   zzaxf = com.google.android.gms.common.api.Api@317047488 (0x12e5c2c0)
|   zzazY = com.google.android.gms.common.api.Api$zzf@315109896 (0x12c83208)
|   zzK = true
|   zzaAh = java.lang.Object@315110136 (0x12c832f8)
|   zzaAi = com.google.android.gms.internal.zzaaf$zza@317493376 (0x12ec9080)
|   zzaAj = java.lang.ref.WeakReference@317491784 (0x12ec8a48)
|   zzaAk = java.util.ArrayList@317491760 (0x12ec8a30)
|   zzaAl = null
|   zzaAm = java.util.concurrent.atomic.AtomicReference@317340960 (0x12ea3d20)
|   zzaAn = null
|   zzaAo = false
|   zzaAp = false
|   zzaAq = null
|   zzaAr = null
|   zzaAs = false
|   zzair = com.google.android.gms.common.api.Status@317061968 (0x12e5fb50)
|   zzazt = com.google.android.gms.common.api.Status@317061968 (0x12e5fb50)
|   zztj = java.util.concurrent.CountDownLatch@317340944 (0x12ea3d10)
|   shadow$_klass_ = com.google.android.gms.internal.zzary$9
|   shadow$_monitor_ = -1987877821
* Instance of com.example.testlocation.BlankFragment
|   static serialVersionUID = 8326097604377836997
|   static $change = null
|   static $classOverhead = byte[1168]@315449345 (0x12cd6001)
|   client = com.google.android.gms.internal.zzaat@317202944 (0x12e82200)
|   textView = android.support.v7.widget.AppCompatTextView@317036544 (0x12e59800)
|   mAdded = false
|   mAllowEnterTransitionOverlap = null
|   mAllowReturnTransitionOverlap = null
|   mAnimatingAway = null
|   mArguments = null
|   mBackStackNesting = 0
|   mCalled = true
|   mCheckedForLoaderManager = false
|   mChildFragmentManager = null
|   mChildNonConfig = null
|   mContainer = null
|   mContainerId = 0
|   mDeferStart = false
|   mDetached = false
|   mEnterTransition = null
|   mEnterTransitionCallback = android.app.SharedElementCallback$1@1883261776 (0x70404b50)
|   mExitTransition = null
|   mExitTransitionCallback = android.app.SharedElementCallback$1@1883261776 (0x70404b50)
|   mFragmentId = 0
|   mFragmentManager = null
|   mFromLayout = false
|   mHasMenu = false
|   mHidden = false
|   mHost = null
|   mInLayout = false
|   mIndex = -1
|   mLoaderManager = null
|   mLoadersStarted = false
|   mMenuVisible = true
|   mNextAnim = 0
|   mParentFragment = null
|   mReenterTransition = android.transition.TransitionSet@1883202880 (0x703f6540)
|   mRemoving = false
|   mRestored = false
|   mRetainInstance = false
|   mRetaining = false
|   mReturnTransition = android.transition.TransitionSet@1883202880 (0x703f6540)
|   mSavedFragmentState = null
|   mSavedViewState = android.util.SparseArray@317491880 (0x12ec8aa8)
|   mSharedElementEnterTransition = null
|   mSharedElementReturnTransition = android.transition.TransitionSet@1883202880 (0x703f6540)
|   mState = 0
|   mStateAfterAnimating = 0
|   mTag = null
|   mTarget = null
|   mTargetIndex = -1
|   mTargetRequestCode = 0
|   mUserVisibleHint = true
|   mView = null
|   mWho = null
|   shadow$_klass_ = com.example.testlocation.BlankFragment
|   shadow$_monitor_ = -2047550015
* Excluded Refs:
| Field: android.view.Choreographer$FrameDisplayEventReceiver.mMessageQueue (always)
| Thread:FinalizerWatchdogDaemon (always)
| Thread:main (always)
| Thread:LeakCanary-Heap-Dump (always)
| Class:java.lang.ref.WeakReference (always)
| Class:java.lang.ref.SoftReference (always)
| Class:java.lang.ref.PhantomReference (always)
| Class:java.lang.ref.Finalizer (always)
| Class:java.lang.ref.FinalizerReference (always)
malodita
  • 53
  • 1
  • 5

2 Answers2

6

This is a documented and long-running issue with the Google Maps SDK leaking memory even after a call to removeLocationUpdates. You can read more about it here.

The following workaround removed the leak canary notification for me.

1) Create a WeakLocationListener class that wraps the location listener in a weak reference and then use this class to handle the onLocationChanged call back.

public class WeakLocationListener implements LocationListener {

private final WeakReference<LocationListener> locationListenerRef;

public WeakLocationListener(@NonNull LocationListener locationListener) {
    locationListenerRef = new WeakReference<>(locationListener);
}

@Override
public void onLocationChanged(android.location.Location location) {
    if (locationListenerRef.get() == null) {
        return;
    }
    locationListenerRef.get().onLocationChanged(location);
}

2) Create an instance of WeakLocationListener (using the LocationListener implemented by your fragment) and use it when you requestLocationUpdates and removeLocationUpdates - the location will be returned to your activity/fragment (or whatever class is implementing LocationListener) and the leak canary notification should disappear.

Hope this helps.

Mr Prezbo
  • 176
  • 1
  • 7
  • I wish that google open sourced the play lib so we could fix things like this! – Jony Thrive Jul 31 '17 at 02:47
  • Started using ReactiveLocation recently - nice library that cuts a lot of boiler plate code out of FusedLocationApi - but it's inherited this memory leak. Hopefully Google fix it soon. – Mr Prezbo Jul 31 '17 at 13:55
  • 1
    Google didn't fix this bug until now (Jan, 31, 2020) – boybeak Jan 31 '20 at 02:44
  • Google has recently resolved this memory leak issue in their latest play-services-location:20.0.0 release. Release Note : https://developers.google.com/android/guides/releases#june_07_2022 – Jatin Jul 25 '22 at 07:03
0

I repaired leak reported by LeakCanary by using weak reference for LocationCallback. It is realy working solution from Mr Prezbo for me. But I had problem where init weak reference. I must put it into onResume method and after that it works like charm. Everywhere where I need I put reference by locationCallback.get()

 private WeakReference<LocationCallback> locationCallback;

 @Override
protected void onResume() {
    super.onResume();
    SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map);
    mapFragment.getMapAsync(this);

    locationCallback = new WeakReference<>(new LocationCallback() {
        @Override
        public void onLocationResult(LocationResult locationResult) {
            if (locationResult != null) {
                onLocationChanged(locationResult.getLastLocation());
            }
        }
    });
}

  @Override
public void onPause() {
    super.onPause();
    Timber.d("Map activity paused...");

    if (locationCallback.get() != null) {
        mFusedLocationClient.removeLocationUpdates(locationCallback.get());
        locationCallback.clear();
    }
}
Andrew Sneck
  • 724
  • 9
  • 18