Friday, September 19, 2014

Managing started service's lifecycle efficiently

So you decided to run your time-consuming operation in service component because you want to ensure that it will continue to run even if the user interface is no longer visible. You chose to extend Service, rather than IntentService because you want your service to handle multiple requests in parallel. In this post I will share a few tips for implementing your service slightly more efficiently.

Android documentation explains that started service must manage its own lifecycle. It also guides us to use framework resources efficiently, and to free once we no longer need them:
A service is "started" when an application component (such as an activity) starts it by calling startService(). Once started, a service can run in the background indefinitely, even if the component that started it is destroyed. Usually, a started service performs a single operation and does not return a result to the caller. For example, it might download or upload a file over the network. When the operation is done, the service should stop itself.
But the given example is not always so simple. The user can request to download several files simultaneously, or even request to cancel the download before it completes. The service, that we initially destined to be stateless, now have to be aware of all the tasks it is currently executing.

Let's imagine how you would implement that. To handle each user request, you will call the startService() method. If the service is not yet started, the service management system will create a brand new service component, and then will call its onStartCommand() method, passing the requested intent. onStartCommand() runs on the main thread, so any non-trivial operation should be executed in a dedicated worker thread.

We want our service to be a good citizen in Android environment, and this means (among other things) that the service should be stopped as soon as it completed all its operations. Android provides service a function (called stopSelfResult) that allows it to stop itself. This method's documentation tells us the following:
Stop the service if the most recent time it was started was startId. This is the same as calling stopService(Intent) for this particular service but allows you to safely avoid stopping if there is a start request from a client that you haven't yet seen in onStart(Intent, int).
Be careful about ordering of your calls to this function.. If you call this function with the most-recently received ID before you have called it for previously received IDs, the service will be immediately stopped anyway. If you may end up processing IDs out of order (such as by dispatching them on separate threads), then you are responsible for stopping them in the same order you received them.
Here comes the tricky part - you should be cautious when calling stopSelfResult(). If you're calling it in the end of asynchronous service operation, sooner or later you'll end up with calling stopSelfResult() with operationB startId before operationA completes (assuming operationA started before operationB).

Yet another thing is to avoid starting and closing the service frequently. We can rationally assume that if user requested an operation to be performed by the service, then there's a high probability that he will request additional operation shortly afterwards.

In summary, our requirements are:
  1. Service will stop itself to free up the resources.
  2. Service will stop itself only if no other operations (even asynchronous) are still running.
  3. Avoid starting/closing the service frequently.
With those requirements in mind I implemented an abstract started service that I'm using as a base class for my concrete service classes.

When I implement new started service, I just extend this BaseService class, add a call to super.onCreate() in concrete service's onCreate() method; and call base class's decreaseReference() function each time asynchronous operation completes.

BaseService is doing several things. It keeps track of last startId, and uses reference counting to determine if all service operations completed and it is safe to call stopSelfResult() with last known startId. Additionally, it postpones service termination for a 20 seconds, which gives an opportunity for other service requests to execute without the overhead of creating the service first.

No comments:

Post a Comment