mapbox / mapbox/mapbox-navigation-android

NavigationView leaks listeners attached via NavigationViewOptions

Open
#3,760 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug UI
Dominant language
Kotlin
Stars
651
Forks
321
PR merge metrics
No merged PRs in 30d

Description

Steps to trigger behavior
  1. Run any activity that attaches listeners that contain context reference via NavigationViewOptions. Reproducible with NavigationViewActivity example.
  2. Rotate the screen.
  3. Leak.
┬───
│ GC Root: System class
│
├─ android.view.inputmethod.InputMethodManager class
│    Leaking: NO (InputMethodManager↓ is not leaking and a class is never leaking)
│    ↓ static InputMethodManager.sInstance
├─ android.view.inputmethod.InputMethodManager instance
│    Leaking: NO (ViewRootImpl↓ is not leaking and InputMethodManager is a singleton)
│    ↓ InputMethodManager.mCurRootView
├─ android.view.ViewRootImpl instance
│    Leaking: NO (MapboxGLSurfaceView↓ is not leaking and ViewRootImpl#mView is not null)
│    ↓ ViewRootImpl.mSurfaceChangedCallbacks
├─ java.util.ArrayList instance
│    Leaking: NO (MapboxGLSurfaceView↓ is not leaking)
│    ↓ ArrayList.elementData
├─ java.lang.Object[] array
│    Leaking: NO (MapboxGLSurfaceView↓ is not leaking)
│    ↓ Object[].[0]
├─ com.mapbox.mapboxsdk.maps.renderer.glsurfaceview.MapboxGLSurfaceView instance
│    Leaking: NO (MapView↓ is not leaking and View attached)
│    mContext instance of com.mapbox.navigation.examples.ui.NavigationViewActivity with mDestroyed = false
│    View.parent com.mapbox.mapboxsdk.maps.MapView attached as well
│    View#mParent is set
│    View#mAttachInfo is not null (view attached)
│    View.mWindowAttachCount = 1
│    ↓ MapboxGLSurfaceView.mParent
├─ com.mapbox.mapboxsdk.maps.MapView instance
│    Leaking: NO (CoordinatorLayout↓ is not leaking and View attached)
│    mContext instance of com.mapbox.navigation.examples.ui.NavigationViewActivity with mDestroyed = false
│    View.parent androidx.coordinatorlayout.widget.CoordinatorLayout attached as well
│    View#mParent is set
│    View#mAttachInfo is not null (view attached)
│    View.mID = R.id.navigationMapView
│    View.mWindowAttachCount = 1
│    ↓ MapView.mParent
├─ androidx.coordinatorlayout.widget.CoordinatorLayout instance
│    Leaking: NO (NavigationView↓ is not leaking and View attached)
│    mContext instance of com.mapbox.navigation.examples.ui.NavigationViewActivity with mDestroyed = false
│    View.parent com.mapbox.navigation.ui.NavigationView attached as well
│    View#mParent is set
│    View#mAttachInfo is not null (view attached)
│    View.mID = R.id.navigationLayout
│    View.mWindowAttachCount = 1
│    ↓ CoordinatorLayout.mParent
├─ com.mapbox.navigation.ui.NavigationView instance
│    Leaking: NO (View attached)
│    mContext instance of com.mapbox.navigation.examples.ui.NavigationViewActivity with mDestroyed = false
│    View.parent androidx.constraintlayout.widget.ConstraintLayout attached as well
│    View#mParent is set
│    View#mAttachInfo is not null (view attached)
│    View.mID = R.id.navigationView
│    View.mWindowAttachCount = 1
│    ↓ NavigationView.navigationViewEventDispatcher
│                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
├─ com.mapbox.navigation.ui.NavigationViewEventDispatcher instance
│    Leaking: UNKNOWN
│    ↓ NavigationViewEventDispatcher.bannerInstructionsListener
│                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~
╰→ com.mapbox.navigation.examples.ui.NavigationViewActivity instance
​     Leaking: YES (ObjectWatcher was watching this because com.mapbox.navigation.examples.ui.NavigationViewActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
​     key = f8ed9703-1913-4718-8392-50b4120e0c70
​     watchDurationMillis = 24175
​     retainedDurationMillis = 19174

METADATA

Build.VERSION.SDK_INT: 30
Build.MANUFACTURER: Google
LeakCanary version: 2.3
App process name: com.mapbox.navigation.examples
Analysis duration: 15309 ms

The issue is, that we're storing references to the listeners (and context that they carry) in the view model. As far as I can tell from a glance at the codebase, we cannot work around the issue as a user. The only way to subscribe to most of the NavigationView listeners is through the options. If we opt to wrap our listeners in a weak reference or anything that avoids carrying the context, then we would lose that reference after rotation and wouldn't be able to re-establish it.

We could try resolving the issue by deprecating subscribing to events via options and expose register/unregister methods instead so that users can manage the state and avoid leaking.

/cc @abhishek1508 @JunDai @cafesilencio

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with NavigationViewActivity and the NavigationViewOptions listener setup, then trace NavigationViewEventDispatcher and the view model that stores those listeners. Reproduce the rotation scenario and inspect the retained-reference path. Done means the listener subscription approach no longer retains a destroyed Activity, with the proposed registration lifecycle clear for callers.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, kotlin
Domain
mobile-dev
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.