Lance Samaria
Lance Samaria

Reputation: 19622

ConnectivityManager Callback Memory Leak - Android

I'm using LeakCanary and it found a memory leak in ConnectivityManager. I've never used ConnectivityManager before and it's not in my project from anything that I wrote. I'm assuming maybe a 3rd party library is using it.

How can I fix this?

┬───
│ GC Root: System class
│
├─ android.net.ConnectivityManager class
│    Leaking: NO (a class is never leaking)
│    ↓ static ConnectivityManager.sCallbackHandler
│                                 ~~~~~~~~~~~~~~~~
├─ android.net.ConnectivityManager$CallbackHandler instance
│    Leaking: UNKNOWN
│    Retaining 32 B in 1 objects
│    ↓ ConnectivityManager$CallbackHandler.this$0
│                                          ~~~~~~
├─ android.net.ConnectivityManager instance
│    Leaking: UNKNOWN
│    Retaining 516.0 kB in 8048 objects
│    mContext instance of com.company.appname.MainActivity with mDestroyed =
│    true
│    ↓ ConnectivityManager.mContext
│                          ~~~~~~~~
╰→ com.company.appname.MainActivity instance
​     Leaking: YES (ObjectWatcher was watching this because com.company.
​     appname.MainActivity received Activity#onDestroy() callback and
​     Activity#mDestroyed is true)
​     Retaining 515.9 kB in 8043 objects
​     key = fb405ad1-a78b-4e8f-8d09-c1b937e1462c
​     watchDurationMillis = 8341
​     retainedDurationMillis = 3230
​     mApplication instance of android.app.Application
​     mBase instance of androidx.appcompat.view.ContextThemeWrapper

enter image description here

Upvotes: 1

Views: 908

Answers (3)

Lance Samaria
Lance Samaria

Reputation: 19622

@KevinCoppock sent me in the right direction

The memory leak culprit was AdMob, specifically MobileAds.initialize(requireContext()) for banner ads which I used inside one of my fragments.

Once I changed it to use MobileAds.initialize(MyApplication.myAppContext), the leak no longer appeared. Here's an example:

1- AdMob initialization in Fragment example:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // MobileAds.initialize(requireContext()) <---- Causes Memory Leak

    MobileAds.initialize(MyApplication.myAppContext) // <---- No Memory Leak
}

2- MyApplication class:

class MyApplication : Application() {

    companion object {
        lateinit  var myAppContext: Context
    }

    override fun onCreate() {
        super.onCreate()

        myAppContext = applicationContext
    }
}

Side note for beginners, make sure to add MyApplication to your Manifest as in android:name=".MyApplication"

Upvotes: 0

Kevin Coppock
Kevin Coppock

Reputation: 134714

There is some discussion on the LeakCanary repo here. In short I believe the issue is that the first initialization of the ConnectivityManager holds a strong reference to the Context it was retrieved by.

So if something (in a 3rd party library for example) initializes it when your MainActivity starts (via getSystemService()) that reference will be leaked.

You could try within your Application class to resolve a ConnectivityManager up front with the application context:

class MyApplication : Application() {
  override fun onCreate() {
    // Eagerly initialize ConnectivityManager with the application context
    // before the Activity is started.
    getSystemService(ConnectivityManager::class.java)
  }
}

Upvotes: 0

Atick Faisal
Atick Faisal

Reputation: 1823

ConnectivityManager in Android is usually used to observe network state changes. To do this, the user needs to register a callback, which will be called by ConnectivityManager when something changes. However, you must unregister this callback when you no longer need the updates. According to the docs:

To avoid performance issues due to apps leaking callbacks, the system will limit the number of outstanding requests to 100 per app (identified by their UID), shared with all variants of this method, of requestNetwork(NetworkRequest, PendingIntent) as well as ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback. Requesting a network with this method will count toward this limit. If this limit is exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, make sure to unregister the callbacks with unregisterNetworkCallback(android.net.ConnectivityManager.NetworkCallback). Requires Manifest.permission.ACCESS_NETWORK_STATE.

I am not sure by this is something that can lead to the leak.

Upvotes: 1

Related Questions