8

I wrote a basic bound service based on the Android documentation, but LeakCanary is telling me the service is leaking.

  1. Is there a leak or have I misconfigured LeakCanary?
  2. How can I write a bound service that does not leak?

The Code

class LocalService : Service() {

  private val binder = LocalBinder()
  private val generator = Random()

  val randomNumber: Int
    get() = generator.nextInt(100)

  inner class LocalBinder : Binder() {
    fun getService(): LocalService = this@LocalService
  }

  override fun onBind(intent: Intent): IBinder {
    return binder
  }

  override fun onDestroy() {
    super.onDestroy()
    LeakSentry.refWatcher.watch(this) // Only modification is to add LeakCanary
  }
}

If I bind to the service from an activity as follows, LeakCanary detects the service has leaked

class MainActivity: Activity() {

  private var service: LocalService? = null
  private val serviceConnection = object: ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
      service = (binder as LocalBinder).getService()
    }
    override fun onServiceDisconnected(name: ComponentName?) {
      service = null
    }
  }

  override fun onStart() {
    super.onStart()
    bindService(Intent(this, LocalService::class.java), serviceConnection, BIND_AUTO_CREATE)
  } 

  override fun onStop() {
    super.onStop()
    service?.let {
      unbindService(serviceConnection)
      service = null
    }
  }
}
┬
├─ com.example.serviceleak.LocalService$LocalBinder
│    Leaking: NO (it's a GC root)
│    ↓ LocalService$LocalBinder.this$0
│                               ~~~~~~
╰→ com.example.serviceleak.LocalService
​     Leaking: YES (RefWatcher was watching this)
Enrico
  • 10,377
  • 8
  • 44
  • 55
  • There is no any flag **BIND_AUTO_START** in *Context* class...why you bind - unbind service in onCreate - onDestroy methods respectively? – Pravin Divraniya May 27 '19 at 10:03
  • You're right, it's a typo. The flag should be `BIND_AUTO_CREATE`. Whether the service is bound and unbound in onCreate and onDestroy is irrelevant; It leaks no matter which lifecycle methods you use to bind and unbind. But I'll change my example to match the Android docs. – Enrico May 28 '19 at 02:01
  • Can you try ... moving Binder class to its own, separate class instead of an inner class and moving the random number generator to that class. Then inside the service, just instantiate it as a field, and return it in onBind(). I'm thinking maybe LeakCanary thinks it's a leak because the an binder inner class holds a reference to the outer class (the service) and that a reference to the binder is being held by another component with it's own lifecycle (the activity). – Ryujin May 28 '19 at 05:30
  • 1
    Inner class is the problem: https://stackoverflow.com/a/49365796/5823014 – János Sicz-Mesziár May 28 '19 at 14:21
  • @JánosSicz-Mesziár I agree, but nothing should be holding a reference to the binder once the service is destroyed – Enrico May 30 '19 at 03:53

1 Answers1

6

I don't know if it's late to answer but after reading your question I also setup leakCanary in my project and found this leak. I was sure that it's because of the inner binder class which is holding the reference of outer class which is service here. That is why in your leak log it shows LocationService is leaking. I found a solution by @commonsguy here and implemented the solution with a bit simpler example here. Hope this helps. Keep coding, stay blessed.

Rookie
  • 304
  • 3
  • 16
  • What is the significant change of the code from the asker to fix the leak? – Alexey Ozerov Oct 21 '21 at 10:08
  • 3
    How are you supposed to access the service object if the binder is external? The binder being an inner class is how it's done in android docs as well. – Sujit Jan 09 '22 at 15:04
  • 1
    @Sujit: I likely could find the solution for this. Keep the explicit reference to the service in the binder instance. When the service is destroying explicitly destroy the binder instance and set the service reference to null there. – Alexey Ozerov Jan 29 '22 at 11:24