Memory leak when Activity hosting ad is destroyed
For this report, I will use the java/admob/NativeAdvancedExample project, but this issue is not specific to that project. It also happens (at least) with WebView banner ads.
In the project above, when the MainActivity is destroyed (as in onDestroy() being called), Leak Canary reports this leak:
D 58:40.489 1 APPLICATION LEAKS
D 58:40.489 References underlined with "~~~" are likely causes.
D 58:40.489 Learn more at https://squ.re/leaks.
D 58:40.489 52701 bytes retained by leaking objects
D 58:40.489 Signature: 91178d655ffcb34658bf9e70d4e86805f1f5cad8
D 58:40.489 ┬───
D 58:40.489 │ GC Root: Global variable in native code
D 58:40.489 │
D 58:40.489 ├─ java.util.HashMap instance
D 58:40.489 │ Leaking: UNKNOWN
D 58:40.489 │ Retaining 1.7 MB in 8478 objects
D 58:40.489 │ ↓ HashMap["ctx"]
D 58:40.489 │ ~~~~~~~
D 58:40.489 ╰→ com.google.example.gms.nativeadvancedexample.MainActivity instance
D 58:40.489 Leaking: YES (ObjectWatcher was watching this because com.google.example.gms.nativeadvancedexample.MainActivity
D 58:40.489 received Activity#onDestroy() callback and Activity#mDestroyed is true)
D 58:40.489 Retaining 52.7 kB in 998 objects
D 58:40.489 key = b5758ba5-b0a0-416f-9e85-9bd32b0afb12
D 58:40.489 watchDurationMillis = 5266
D 58:40.489 retainedDurationMillis = 263
D 58:40.489 mApplication instance of android.app.Application
D 58:40.489 mBase instance of androidx.appcompat.view.ContextThemeWrapper
I have made slight modification to the NativeAdvancedExample to make it easily reproducible:
- it adds the Leak Canary dependency to
build.gradle - it disables the ad inspector in Manifest so it does not get in the way
- it adds a 'Destroy Activity' button to the
MainActivitythat just callsfinish()so theMainActivityis destroyed andonDestroyed()is called (which is necessary to trigger this leak). Adding this button is necessary because otherwise just doing the back action to close theMainActivitywill only trigger itsonPause().
To reproduce the leak trace above:
- download and unzip the modified NativeAdvancedExample project
- run
./gradlew assembleDebug - install app:
adb install app/build/outputs/apk/debug/app-debug.apk - launch the
Admob Native Advancedapp - display an ad
- tap the
Destroy Activitybutton - wait for a bit for Leak Canary to do its job (up to 1 or 2 minutes) and after a while the logcat for package
com.google.example.gms.nativeadvancedexamplewill displayed the trace above.
Interestingly, if in MainActivity.java you change this line:
AdLoader.Builder builder = new AdLoader.Builder(this, ADMOB_AD_UNIT_ID);
to:
AdLoader.Builder builder = new AdLoader.Builder(getApplicationContext(), ADMOB_AD_UNIT_ID);
(that is, using the Application Context rather than the Activity Context for the AdLoader)
The leak trace will be slightly different with a reference to the NativeAdView:
D 54:06.559 1 APPLICATION LEAKS
D 54:06.559 References underlined with "~~~" are likely causes.
D 54:06.559 Learn more at https://squ.re/leaks.
D 54:06.559 1664381 bytes retained by leaking objects
D 54:06.559 Signature: 91178d655ffcb34658bf9e70d4e86805f1f5cad8
D 54:06.559 ┬───
D 54:06.559 │ GC Root: Global variable in native code
D 54:06.559 │
D 54:06.559 ├─ java.util.HashMap instance
D 54:06.559 │ Leaking: UNKNOWN
D 54:06.559 │ Retaining 1.7 MB in 8405 objects
D 54:06.559 │ ↓ HashMap["view"]
D 54:06.559 │ ~~~~~~~~
D 54:06.559 ├─ com.google.android.gms.ads.nativead.NativeAdView instance
D 54:06.559 │ Leaking: YES (View.mContext references a destroyed activity)
D 54:06.559 │ Retaining 1.7 MB in 8271 objects
D 54:06.559 │ View is part of a window view hierarchy
D 54:06.559 │ View.mAttachInfo is null (view detached)
D 54:06.559 │ View.mWindowAttachCount = 1
D 54:06.559 │ mContext instance of com.google.example.gms.nativeadvancedexample.MainActivity with mDestroyed = true
D 54:06.559 │ ↓ View.mContext
D 54:06.559 ╰→ com.google.example.gms.nativeadvancedexample.MainActivity instance
D 54:06.559 Leaking: YES (ObjectWatcher was watching this because com.google.example.gms.nativeadvancedexample.MainActivity
D 54:06.559 received Activity#onDestroy() callback and Activity#mDestroyed is true)
D 54:06.559 Retaining 49.4 kB in 934 objects
D 54:06.559 key = 806db37f-9aad-481d-9c0a-96b1f2566cc8
D 54:06.559 watchDurationMillis = 5412
D 54:06.559 retainedDurationMillis = 412
D 54:06.559 mApplication instance of android.app.Application
D 54:06.559 mBase instance of androidx.appcompat.view.ContextThemeWrapper
So the big question, is where does this HashMap comes from and why does it retain an instance of NativeAdView (or directly the MainActivity context in the first trace) after the Activity is destroyed ?
There is no fix or workaround I can think of, the leak has to be in the AdMob SDK...
Please Report this issue to SDK Support :
https://groups.google.com/g/google-admob-ads-sdk
Already did a while ago:
https://groups.google.com/g/google-admob-ads-sdk/c/2_xl6PpJhXg/m/KxItALS0AAAJ
@bubbleguuum were you able to fix it?? https://github.com/square/leakcanary/issues/2688
This is real leak right?
Yes AFAICT it is a real leak. Though I did not check again since the date of this report and it may have been fixed in the AdMob SDK (though I highly doubt it).