23

I am using AdMob in a fragment. Sometimes I see the following stack

10-23 14:27:38.916: E/ActivityThread(21250): Activity com.applegrew.app.skywifiremote.MainActivity has leaked ServiceConnection com.google.android.gms.common.b@420e82e8 that was originally bound here
10-23 14:27:38.916: E/ActivityThread(21250): android.app.ServiceConnectionLeaked: Activity com.applegrew.app.skywifiremote.MainActivity has leaked ServiceConnection com.google.android.gms.common.b@420e82e8 that was originally bound here
10-23 14:27:38.916: E/ActivityThread(21250):    at android.app.LoadedApk$ServiceDispatcher.<init>(LoadedApk.java:979)
10-23 14:27:38.916: E/ActivityThread(21250):    at android.app.LoadedApk.getServiceDispatcher(LoadedApk.java:873)
10-23 14:27:38.916: E/ActivityThread(21250):    at android.app.ContextImpl.bindServiceCommon(ContextImpl.java:1690)
10-23 14:27:38.916: E/ActivityThread(21250):    at android.app.ContextImpl.bindService(ContextImpl.java:1673)
10-23 14:27:38.916: E/ActivityThread(21250):    at android.content.ContextWrapper.bindService(ContextWrapper.java:517)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.gms.ads.identifier.a.b(SourceFile:179)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.gms.ads.identifier.a.a(SourceFile:207)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.a.t.d(SourceFile:83)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.a.t.b(SourceFile:131)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.a.q.a(SourceFile:258)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.a.q.a(SourceFile:195)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.gms.ads.internal.k.a(SourceFile:76)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.gms.ads.internal.request.c.f_(SourceFile:99)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.gms.ads.internal.util.b.run(SourceFile:17)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.gms.ads.internal.util.d.call(SourceFile:29)
10-23 14:27:38.916: E/ActivityThread(21250):    at com.google.android.gms.ads.internal.util.e.call(SourceFile:49)
10-23 14:27:38.916: E/ActivityThread(21250):    at java.util.concurrent.FutureTask.run(FutureTask.java:237)
10-23 14:27:38.916: E/ActivityThread(21250):    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
10-23 14:27:38.916: E/ActivityThread(21250):    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
10-23 14:27:38.916: E/ActivityThread(21250):    at java.lang.Thread.run(Thread.java:841)

From stack trace it looks like the source of leak is AdMob code. However, in my fragment I have code to destroy the AdMob view when the fragment is destroyed.

Snippet from my fragment.

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    initAd();
}

private void initAd() {
    mAdView = (AdView) getView().findViewById(R.id.remote_pager_ad);
    if (mAdView != null) {
        AdRequest adRequest = new AdRequest.Builder().addTestDevice(
                AdRequest.DEVICE_ID_EMULATOR).build();
        mAdView.loadAd(adRequest);
    }
}

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

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

@Override
public void onDestroy() {
    mAdView.destroy();
    super.onDestroy();
}
AppleGrew
  • 9,302
  • 24
  • 80
  • 124
  • Can you add the whole fragment code? – Simas Oct 23 '14 at 09:41
  • 1
    @AppleGrew, I ran into a similar issue with AdMob (Google Play Services) calling `registerComponentCallbacks()` and never unregistering it. To get around that, I override `registerComponentCallbacks()` (and `unregisterComponentCallbacks()`) in my Application subclass, and keep a list of everything that registers itself (and remove those that properly unregister). In my base Activity's `onDestroy`, I invoke a new method in my Application class that then forcefully unregisters those callbacks that haven't unregistered themselves. This solved a major memory leak in my application. – Joe Dec 01 '14 at 23:52
  • @Joe could you elaborate a bit more? Your comment seems very useful, maybe post it as an answer with example code? For example, how to unregister the leaked callbacks? – pertz Dec 02 '14 at 00:45
  • OK, thought the "unregisterComponentCallbacks" was a callback, not a real "callable" method, so I guess that to unregister I should just call it... well, I'll try it, this Google Play leak is killing me. Thanks! – pertz Dec 02 '14 at 00:57
  • @rottz: code sample posted as an answer: http://stackoverflow.com/a/27253968/231078 – Joe Dec 02 '14 at 16:16
  • Probably caused by the following bug, known by the admob team (but maybe worth to add a comment there so they know it's a wide-spread problem): https://groups.google.com/forum/#!topic/google-admob-ads-sdk/eKZzvQgRiL8 – Gavriel Jan 18 '15 at 19:14

2 Answers2

6

I was having a similar issue with AdMob (Google Play Services) ads leaking huge amounts of memory. Used MAT to find that the problem was every gms ad was being retained by my Application instance, in the sComponentCallbacks array. So I override registerComponentCallbacks() and unregisterComponentCallbacks() to keep track of instances that are registering themselves but never unregistering (when I assume they should have done so). This code example assumes only the .gms.ads package is problematic, but if you find others that are causing a similar leak, you can add those packages to the list as well:

public class MyApplication extends Application {
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void registerComponentCallbacks(ComponentCallbacks callback) {
        super.registerComponentCallbacks(callback);
        ComponentCallbacksBehavioralAdjustmentToolIcs.INSTANCE.onComponentCallbacksRegistered(callback);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void unregisterComponentCallbacks(ComponentCallbacks callback) {
        ComponentCallbacksBehavioralAdjustmentToolIcs.INSTANCE.onComponentCallbacksUnregistered(callback);
        super.unregisterComponentCallbacks(callback);
    }

    public void forceUnregisterComponentCallbacks() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            ComponentCallbacksBehavioralAdjustmentToolIcs.INSTANCE.unregisterAll(this);
        }
    }


    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private static class ComponentCallbacksBehavioralAdjustmentToolIcs {
        static ComponentCallbacksBehavioralAdjustmentToolIcs INSTANCE = new ComponentCallbacksBehavioralAdjustmentToolIcs();

        private WeakHashMap<ComponentCallbacks, ApplicationErrorReport.CrashInfo> mCallbacks = new WeakHashMap<>();
        private boolean mSuspended = false;

        public void onComponentCallbacksRegistered(ComponentCallbacks callback) {
            Throwable thr = new Throwable("Callback registered here.");
            ApplicationErrorReport.CrashInfo ci = new ApplicationErrorReport.CrashInfo(thr);

            if (BuildConfig.DEBUG) Log.w(TAG, "registerComponentCallbacks: " + callback, thr);

            if (!mSuspended) {
                if (callback.getClass().getName().startsWith("com.google.android.gms.ads")) {
                    mCallbacks.put(callback, ci);
                }
                // TODO: other classes may still prove to be problematic?  For now, only watch for .gms.ads, since we know those are misbehaving
            } else {
                if (BuildConfig.DEBUG) Log.e(TAG, "ComponentCallbacks was registered while tracking is suspended!");
            }
        }

        public void onComponentCallbacksUnregistered(ComponentCallbacks callback) {
            if (!mSuspended) {
                if (BuildConfig.DEBUG) {
                    Log.i(TAG, "unregisterComponentCallbacks: " + callback, new Throwable());
                }

                mCallbacks.remove(callback);
            }
        }

        public void unregisterAll(Context context) {
            mSuspended = true;

            for (Map.Entry<ComponentCallbacks, ApplicationErrorReport.CrashInfo> entry : mCallbacks.entrySet()) {
                ComponentCallbacks callback = entry.getKey();
                if (callback == null) continue;

                if (BuildConfig.DEBUG) {
                    Log.w(TAG, "Forcibly unregistering a misbehaving ComponentCallbacks: " + entry.getKey());
                    Log.w(TAG, entry.getValue().stackTrace);
                }

                try {
                    context.unregisterComponentCallbacks(entry.getKey());
                } catch (Exception exc) {
                    if (BuildConfig.DEBUG) Log.e(TAG, "Unable to unregister ComponentCallbacks", exc);
                }
            }

            mCallbacks.clear();
            mSuspended = false;
        }
    }
}

Then in my BaseActivity's onPause() (or onDestroy()) method, I call my forceUnregisterComponentCallbacks() method:

@Override
public void onPause() {
    ((MyApplication) getApplicationContext()).forceUnregisterComponentCallbacks()
    super.onPause();
}

Note that ComponentCallbacks was introduced in ICS, so if you're seeing problems on versions before ICS, then this is not the problem.

(I also realize that this does not address the exact problem identified in the OP, as that has to do with bindService(), not ComponentCallbacks. But it saved us from a pretty major memory leak that forced us to disable AdMob entirely until a hotfix could be released.)

Joe
  • 42,036
  • 13
  • 45
  • 61
  • Brilliant hack, thanks for sharing! I hope it doesn't break something else in my app. In my case [WebView's `ComponentCallbacks` isn't unregistering itself even though I've called `destroy()` on it](https://github.com/vickychijwani/quill/issues/75#issuecomment-115012824), and it's holding on to my Activity causing massive memory leaks. – Vicky Chijwani Jun 24 '15 at 22:15
  • One year later: I discovered this was because of a programming error on my part. Apparently [`WebView` needs to be removed from the view hierarchy before being `destroy()`ed](https://developer.android.com/reference/android/webkit/WebView.html#destroy%28%29). Doing that appears to fix the leak without resorting to this hack. – Vicky Chijwani May 22 '16 at 21:31
1

It's seen like you need to unregister the Service before your Activity lost the context! This's the same issue for Dialog's when you call dismiss after Activity has lost. :-/