Most common error I’ve encountered with the Android Service component.

I’ve encountered many different Android projects over the years, and one of the things that keep coming up as something most teams struggle with is how to use the Android Service component.

In this and following posts I’m going to cover how the Service works, the common pitfalls about it, when it’s appropriate to use it, and examples of cases where you shouldn’t.

I recently posted a Twitter poll about the behaviour of the onServiceDisconnected() method in ServiceConnection interface.

The correct answer is “Never”, and this post will cover the part about how bindings for the Android Service component works.


It should be worrying for Google that more than 3/4 of the respondents got the answer wrong to my Twitter poll. The Service component is maybe not the most used one in Android today, but when used, it usually serves a very important task that shouldn’t easily fail. Since a Service is usually used to handle tasks that happens when the app is running in the background (more on this later), an error here might not be noticed by the user. If the Service would be something like a companion app to a smart watch, the user might be missing important information that they expect to receive.

Basically, getting your Service right is usually very important for the apps that uses them.

Binding to a Service

There are basically two ways to interact with a Service, either by sending messages to it in the form of an Intent and using the startService method, or by binding to it and performing RPC-like interactions with it. Interacting with a Service works both when it runs in the same process (e.g., a local Service) or when running in a different process or even a different app (e.g., a remote Service).

In this post we will look at binding to a Service and discover the various pitfalls involved.

Binding to a Service is done by calling Context.bindService() like this.

val bindIntent = Intent(activity, MyService::class.java)
bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE)

The first parameter, bindIntent, identifies the Service we want to bind to in the same way as we target another Activity to start. The Intent could just contain an action, which you then would have to include in the manifest entry for your Service.

The second parameter is our ServiceConnection which contains a callback used to signal when we connect and gets disconnected from a Service. More on this in a moment.

The final parameter is a flag that signal how the Service should behave from this binding. This parameter is only relevant when binding to a remote Service, in which case you usually at least want it to be BIND_AUTO_CREATE. I’ll cover more on this later on.

ServiceConnection

The state of a connection to a bound Service is indicated through the callbacks in the ServiceConnection interface. Here is where I’ve seen most of the bugs around the user of Android appear. While all bindings usually will result in a call to onServiceConnected, you will only receive a call to onServiceDisconnected for a remote Service (that is, it is running in a different process). Don’t expect the callback to happen when you call unbindService() in your code!

You will never receive a call to onServiceDisconnected() for a Service running in the same process that you’re binding from.

A very common use of ServiceConnection that I’ve seen looks something like this.

class MyServiceConnection : ServiceConnection {
    var binder: MyService.MyBinder? = null
    
    override fun onServiceConnected(name: ComponentName?, 
                                    binder: IBinder?) {
        this.binder = binder as MyService.MyBinder
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        binder = null
    }
}

The binder parameter we receive in onServiceConnected() is a special type of object that allows RPC calls across process on Android. For a local Service, we usually just cast it to the type returned in the onBind() method of our Service (we’ll look at that in a moment), and store a reference locally so we can invoke calls in our Service through that.

The bug that is so common is that the onServiceDisconnected() is assumed to be called once we unbind, and then it will be used for clearing the reference to our binder. However, since this callback is never invoked for a local Service, this will never happen and we will effectively leak a reference. This is not fatal in itself, but if you have logic in your app to determine if you need to bind to the Service again and that is based on the binder being null or not, you will run into some strange behaviour.

I’ve also seen a lot of cases where additional logic is placed in onServiceDisconnected, which is then never called.

Remote services

But how about a remote Service? When is the onServiceDisconnected called in this case?

Well, it turns out that in normal situations, it will never be called either. When you call unbindService() when it’s time to release the binding, you won’t get a call to onServiceDisconnected even though it is running in a remote process. The only time you will get a call to onServiceDisconnected() is when the remote process of the Service you’re binding to unexpectedly dies.

There are only three cases where this happens. The first case is hopefully the less common; the remote process crashes due to a bug. The second case is when the system shuts down the remote process because it is updating the app that it was running. This is a case you need to handle if you are integrating Play Services Billing API using the provided AIDL. The third case is when the system shuts down the remote process due to memory constraints. This can be avoided by passing the correct flags to bindService.

This doesn’t mean you should care about onServiceDisconnected() for a remote Service binding. However, it is important that you are aware of when this is called and when it’s not called. Don’t build your app under the assumption that onServiceDisconnected is a signal for cleaning up references, but instead use this as an indication that something went wrong on the remote side.

Binding flags

The third parameter to bindService is a flag that signals how the Service should behave for this binding. For a remote Service, this can affect when and how the remote process gets killed when the device is running low on memory. However, regardless if it’s a remote or local Service, you need to use the flag BIND_AUTO_CREATE or the actual Service component won’t be created (at least until your start it explicitly with startService()).

Normally, the remote process that you bind will remain running as long as the binding process is running in the foreground (that is, it has a visible Activity). However, when the OS detects that it needs to kill processes due to low memory, you should make sure that you bound to the Service with the flag BIND_ABOVE_CLIENT or BIND_ADJUST_WITH_ACTIVITY. Check the documentation for the differences between these. Note however that there is no guarantee that the remote process won’t be killed anyway. This is just a hint.

If you bind to a remote Service, you should use the following flags:

val flags = Context.BIND_AUTO_CREATE or Context.BIND_ABOVE_CLIENT
bindService(intent, serviceConnection, flags)

android.app.Service

So far, we’ve only looked at the client side of binding to a Service. Let’s have a look at the code inside your Service class.

The most basic Service you can create would look something like this.

class MostBasicService : Service() {
    override fun onBind(intent: Intent?): IBinder? {
        return null
    }
}

The only method you must override for a Service is onBind(). If this method return null, you will never receive a callback to onServiceConnected(). This effectively means that a Service that returns null in onBind() will not be possible to bind to.

Here comes another problem I often encountered. The response from onBind() is cached by the system. Until the Service has been destroyed (i.e., onDestroy() is called), you will never receive a new call to onBind() for any consecutive bindService() call for that Service.

How can this be a problem? Let’s say you lazily initiate the IBinder object that you return from onBind(). For example, you first start the Service which then triggers an onStartCommand(), which in turn creates your IBinder object. Since all this is asynchronous you risk having a Service that calls onBind() before the lazy initialization of the IBinder object is complete. Now you will have a Service you cannot bind to without first explicitly destroying the Service.

I’ve seen a number of cases where apps used different processes for the UI and the Service, and this pattern of lazily creating the IBinder was used to set things up.

The correct way to deal with this, regardless if you have a local or remote Service, is to create the IBinder object when the Service instance is created. You can do it in onCreate() but a better option is to have it act as a simply proxy and simply instantiate as the object is created, like this.

class MostBasicService : Service() {
    val binder = SimpleBinder()
    
    override fun onBind(intent: Intent?): IBinder? {
        return binder
    }
    
    inner class SimpleBinder : Binder {
        val service = this@MostBasicService
    } 
}

The code above works for a local Service only, for a remote Service you need an AIDL file that you implement as the IBinder object. I’m leaving that as an exercise to the reader.

Since onBind() is only called once as long as you have at least one active binding, it might be good to know that onUnbind() will only be called once all bindings for the current Service has unbound (i.e., called unbindService()).

The final part of the binding methods in a Service class is onRebind(). Most Android developers will most likely never use this in their apps, regardless if they implement a remote or local Service. The method is only relevant if you override the onUnbind() method of you Service and return true from that.

If your onUnbind() return true, your onRebind() method will be called when a new client binds after all previous client had disconnected.

A final detail about onBind() (as well as onRebind()). This method receives the Intent you used for binding. This is another place where I’ve seen Android developers struggle with implementing a Service. You cannot pass extras in the Intent used for binding to a Service. All extras will be stripped before passed to onBind().

Summary

That are the challenges I’ve encountered with binding to a Service. Let’s sum them up;

  • onServiceDisconnected() will never be called for a local Service and only in exceptional cases for a remote Service. Don’t rely on that for anything critical.
  • At the very least, use Context.BIND_AUTO_CREATE as a flag to your Service or it won’t be created from binding to it and you won’t receive a call to onServiceConnected().
  • When binding to a remote Service, also use the flag BIND_ABOVE_CLIENT to ensure it has the same priority as your own process. Note that this is not a guarantee it won’t be killed by the system.
  • onBind() will have its result cached by the system and will only be invoked for the first client that binds. The same is true for onUnbind(), which will only be called when the last clients unbinds.

Today, there is very little point of binding to a Service. As I will show in a later post, there are much better ways of interacting with a Service that doesn’t require you to use any of the complexities on binding.

The few occasions where you will need to bind to a Service is for some APIs and SDKs where they have been designed that way. Most notably here is the Billing API in Play Services.

In the next post we will look at how starting and stopping a Service works and what to consider when using that method instead of binding.