square / leakcanary

A memory leak detection library for Android.
https://square.github.io/leakcanary
Apache License 2.0
29.31k stars 3.97k forks source link

How to properly exclude a class (service) ? #2159

Open maxxx opened 3 years ago

maxxx commented 3 years ago

Hello! I'm trying to exclude 3rd party lib memleak. This is the config:

List<ReferenceMatcher> leakCanaryMatchers = new ArrayList<>(LeakCanary.getConfig().getReferenceMatchers());
leakCanaryMatchers.add(AndroidReferenceMatchers.instanceFieldLeak("me.pushy.sdk.services.PushyJobService",
        "this$0", // ??? this\\$0 and this doesn't helps
        "Pushy service",
        ignore -> true));

LeakCanary.setConfig(new LeakCanary.Config.Builder(LeakCanary.getConfig())
        .referenceMatchers(leakCanaryMatchers).build());

And this is the memory leak report.

┬───
    │ GC Root: Local variable in native code
    │
    ├─ java.lang.Thread instance
    │    Leaking: UNKNOWN
    │    Retaining 205 B in 5 objects
    │    Thread name: 'MQTT Call: fc307498c082fe03e987c4'
    │    ↓ Thread.target
    │             ~~~~~~
    ├─ me.pushy.sdk.lib.paho.internal.CommsCallback instance
    │    Leaking: UNKNOWN
    │    Retaining 296 B in 10 objects
    │    ↓ CommsCallback.mqttCallback
    │                    ~~~~~~~~~~~~
    ├─ me.pushy.sdk.util.PushyMqttConnection instance
    │    Leaking: UNKNOWN
    │    Retaining 2.9 kB in 27 objects
    │    mContext instance of me.pushy.sdk.services.PushyJobService
    │    ↓ PushyMqttConnection.mContext
    │                          ~~~~~~~~
    ╰→ me.pushy.sdk.services.PushyJobService instance
        Leaking: YES (ObjectWatcher was watching this because me.pushy.sdk.services.PushyJobService received
        Service#onDestroy() callback and Service not held by ActivityThread)
        Retaining 2.8 kB in 24 objects
        key = 1700a845-3541-40e5-ada1-719edb299264
        watchDurationMillis = 36527
        retainedDurationMillis = 31522
        mApplication instance of com.***.Application
        mBase instance of android.app.ContextImpl

    METADATA

    Build.VERSION.SDK_INT: 28
    Build.MANUFACTURER: Google
    LeakCanary version: 2.7
    App process name: com.***
    Stats: LruCache[maxSize=3000,hits=4583,misses=76476,hitRate=5%]
    RandomAccess[bytes=4308515,reads=76476,travel=41209829865,range=27177049,size=36754164]
    Heap dump reason: 1 retained objects, app is not visible
    Analysis duration: 16807 ms

Problem - the memleak is still reported, I can find it in the "leaks" section (separate launcher icon). I haven't found any info about excluding leaks except https://square.github.io/leakcanary/recipes/#matching-known-library-leaks leakcanary v2.7

pyricau commented 2 years ago

We definitely should beef up the documentation here.

So, your problem is that the PushyService is leaking, because the mqtt connection is still running and referencing it as a context instead of using the application context. You should reach out to them and ask them to fix it. I just filed something here: https://github.com/pushy-me/pushy-demo-android/issues/17

The updated config you used should work, and I think it did, however you excluded one path and LeakCanary found another path for that same leak. And it could find many other paths!

Your key issue here is that you don't want LeakCanary to track any PushyService instance. This is only documented in the changelog for now: https://square.github.io/leakcanary/changelog/#configuring-retained-object-detection

class DebugExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()

    val delegate = ReachabilityWatcher { watchedObject, description ->
      if (watchedObject !is PushyService) {
        AppWatcher.objectWatcher.expectWeaklyReachable(watchedObject, description)
      }
    }

    val watchersToInstall = AppWatcher.appDefaultWatchers(application, delegate)

    AppWatcher.manualInstall(
      application = application,
      watchersToInstall = watchersToInstall
    )
  }
}
top-master commented 1 year ago

Similar issue here, where I need to exclude a single member-variable of a single class.

But I want to configure existing default watcher, instead of disabling default, and installing own custom watcher.

@pyricau Even if you "beef up the documentation" what I said is currently not supported, is it???

Being forced to do all that for a single member-variable seems to be far too much.

top-master commented 1 year ago

@pyricau Any update on how to "configure existing default watcher"?

Also, your online documentation seems to be down (at time of writting), hence I downloaded.

But could not find any answer yet.

pyricau commented 1 year ago

You can install your own watcher, yes.

top-master commented 1 year ago

"can install your own watcher"

@pyricau Okay, I guessed you may know.

But for anyone else interested, with v2.9 (and maybe later) here is how:

import leakcanary.LeakCanary;
import shark.AndroidReferenceMatchers

// ...

// Fetch existing defaults.
val matchers = AndroidReferenceMatchers.appDefaults.toMutableList()

// Configure.
matchers += AndroidReferenceMatchers.instanceFieldLeak("my_package.MyClass", "myVariable")

// Save and/or apply changes.
LeakCanary.config = LeakCanary.config.copy(
        referenceMatchers = matchers
)

Note that above's in Kotlin, because doing the same with Java is far too complicated, hence consider Leak-canary to support Kotlin only.

That's no issue for me, I write in Kotlin and call it from Java.