ConnectivityManager Callback Memory Leak - Android

394 Views Asked by At

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

3

There are 3 best solutions below

0
Lance Samaria On BEST ANSWER

@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"

3
Atick Faisal On

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.

5
Kevin Coppock On

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)
  }
}