Chapter 6. Long-running Tasks with Service

In Chapter 5, Queuing Work with IntentService, we learned about a specialized subclass of Service that handles its workload on a single background thread.

In this chapter, we'll extend our toolkit by directly extending Service to take control of the level of concurrency applied to our long-running background tasks—how many threads are used to perform the work—and use various methods to send work to Services and receive results from them.

In this chapter, we will cover the following topics:

  • Building responsive apps with Service
  • Controlling concurrency with Executors
  • Returning results with Messenger
  • Direct communication with local Services
  • Broadcasting results with Intents
  • Detecting unhandled broadcasts
  • Applications of Services

Building responsive apps with Service

Throughout this book, we have learned about the concurrency constructs provided by the Android platform for doing work off the main thread. So, it might seem surprising that, by itself, Service does not provide any background threads and will run all of its callbacks directly on the main thread, just like Activity.

If we perform long-running work or block the main thread in a Service callback method, our application may be shut down, and a system-triggered Application Not Responding dialog is presented to the user.

While it is possible to configure the service to launch in a separate process, that process will still run the Service callbacks on its own main thread and will be subject to the same constraints. The only difference is that our foreground process will not be shut down along with the misbehaving Service process.

The solution, of course, is to pass the work off from the main thread to background "worker" threads.

IntentService, which we learned about in the previous chapter, does exactly this by passing work from the main thread to a single background HandlerThread—an elegant design and a nice example of using the platform concurrency constructs as building blocks to compose concurrent applications.

Elegant though it may be, we can only invoke IntentService via an Intent and it will queue all work and process it on a single thread. These design choices make IntentService easy to use, but can also be limiting.

When we need more control over the level of concurrency, or alternative methods to trigger long-running background work, we can create our own Service implementations and take complete control.

Controlling concurrency with Executors

The Executor interface was introduced to the core Java libraries in Java 5 as a means of submitting tasks to be executed without specifying exactly how or when the execution will be carried out. We learned about Executor briefly in Chapter 2, Staying Responsive with AsyncTask, and used it to control the level of concurrency of our AsyncTasks.

In this section, we'll use Executor to create our own alternative to IntentService, initiating background work in a Service by sending it an Intent, but retaining control over the level of concurrency. Like IntentService, our class will be abstract and should be subclassed to define the actual behavior. So, we'll define an abstract onHandleIntent method for the subclasses to implement:

public abstract class ConcurrentIntentService extends Service {
  protected abstract void onHandleIntent(Intent intent); 
}

We must override the onBind method of Service, but for now, we will return null, as we won't be using it just yet:

@Override
public IBinder onBind(Intent intent) {
  return null;
}

We'll allow subclasses to define the level of concurrency by passing an Executor to the constructor.

public abstract class ConcurrentIntentService extends Service {
  private final Executor executor;
  public ConcurrentIntentService(Executor executor) {
    this.executor = executor;
  }

    protected abstract void onHandleIntent(Intent intent);
}

The onHandleIntent method will be invoked in the background using the Executor and triggered from the onStartCommand method of our Service, which is invoked by the platform when we call startService from our Activity.

@Override
public int onStartCommand(
  final Intent intent, int flags, int startId) {      
  executor.execute(new Runnable(){
    @Override
    public void run() {
      onHandleIntent(intent);                
    }
  });
  return START_REDELIVER_INTENT;
}

Note that we're returning START_REDELIVER_INTENT from onStartCommand, which tells the system that if it must kill our Service, for example, to free up memory for a foreground application, it should be scheduled to restart when the system is under less pressure and the last Intent object sent to the Service should be redelivered to it.

Another sensible return value here might be START_NOT_STICKY, which would mean the Service is not automatically restarted and Intents are not redelivered. The third common flag—START_STICKY—is not appropriate to replicate the behavior of IntentService, because we don't want to restart this Service without an Intent to process.

That's all we really need to do to implement our concurrent Intent-driven Service, but we should be responsible and stop the Service when it has no more work to do. We'll need to keep track of how many tasks are running at any given time and when the last one completes, invoke stopSelf.

To track tasks, we'll add a simple int counter property to the class and increment it immediately when we receive a new Intent. We'll use a Handler created on the main thread to send messages from the background threads to the main thread when a task completes. This Handler will decrement the counter again and stop the Service when the count reaches zero. By doing all of the tracking in a Handler on the main thread, we avoid any synchronization issues.

private final CompletionHandler handler = 
  new CompletionHandler();
private int counter;

@Override
public void onStart(final Intent intent, int startId) {
  counter++;
  executor.execute(new Runnable(){
      @Override
      public void run() {
        try {
          onHandleIntent(intent);
        } finally {
          handler.sendMessage(Message.obtain(handler));
        }
      }
  });
}
    
private class CompletionHandler extends Handler {
  @Override
  public void handleMessage(Message msg) {
    if (--counter == 0) {
      Log.i(TAG, "0 tasks, stopping");
      stopSelf();
    } else {
      Log.i(TAG, counter + " active tasks");
    }
  }
}

We can now use our multithreaded Intent-driven Service exactly as we used IntentService. We subclass it, this time passing an Executor configured to run tasks in as many threads as we need, then invoke it from our Activities and Fragments using Intents.

Using our new ConcurrentIntentService, we can make some very small modifications to PrimesIntentService from Chapter 5, Queuing Work with IntentService, to have it perform its calculations using a pool of threads:

public class PrimesIntentService extends ConcurrentIntentService {
  public static final int MAX_CONCURRENCY = 5;
  public static final String PARAM = "prime_to_find";
  
  public PrimesIntentService() {
    super(Executors.newFixedThreadPool(
      MAX_CONCURRENCY,
      new ThreadFactory(){
        @Override
        public Thread newThread(Runnable r) {
          Thread t = new Thread(r);
          t.setPriority(Thread.MIN_PRIORITY);
          t.setName("primes-intent-thread");
          return t;
        }
      }));
      }

    // … the rest of the class is unchanged from chapter 5
}

The changes we made are very small. We now extend ConcurrentIntentService instead of IntentService, and we used a factory method from the Executor JDK class to create a fixed-size pool of threads to run our tasks with.

Note that we supplied a ThreadFactory and used it to set the priority of the threads in our pool to Thread.MIN_PRIORITY. It is crucial to do this so that our background worker threads don't starve the main thread of CPU time.

Tip

An early warning sign that our worker threads are contending too heavily with the main thread is a log message like this:

I/Choreographer﹕ Skipped 53 frames!  The application may be doing too much work on its main thread.

Despite the message accusing the application of doing too much on the main thread, it may also be produced if the application is doing too much work in worker threads which have too high a priority, and as a result compete too strongly with the main thread for CPU time.

We can invoke our new concurrent version of PrimesIntentService just as we invoked the single-threaded version in the previous chapter by calling startService with an Intent:

Intent intent = new Intent(this, PrimesIntentService.class);
intent.putExtra(PrimesIntentService.PARAM, primeToFind);
startService(intent);

Returning results with Messenger

When we're doing work in a Service on behalf of an Activity, it is very common to want to send results back to that Activity, even across configuration changes that cause restarts. Wouldn't it be ideal if we could give our PrimesIntentService a Handler that it can use to send messages for processing in the context of the Activity? The great news is we can!

The Android framework provides the Messenger class, which wraps up Handler and makes it possible to send messages from anywhere—including from remote Services in other processes. Messenger implements Parcelable, which means we can pass Messengers around in Intents, which we can't do with Handler directly.

Let's update our Service and Activity to communicate results using Messenger!

In PrimesIntentService, we'll add a static final int member to ensure that we use consistent values for the message.what property and a static final String member to use as the Intent's extra key when passing our Messenger.

public static final int RESULT = "nth_prime".hashCode();
public static final String MSNGR = "messenger";

The first thing we'll need in the Activity is a Handler subclass that can process the result messages. We'll define it as a static inner class of our Activity, so that we don't accidently leak implicit references to the enclosing class.

private static class PrimesHandler extends Handler {
  private TextView view;
  public void handleMessage(Message message) {
    if (message.what == PrimesIntentService.RESULT) {
      if (view != null) { // if we're attached
        view.setText(message.obj.toString());
      }
    }
  }
  public void attach(TextView view) {
    this.view = view;
  }
  public void detach() {
    this.view = null;
  }
}

Our Handler member is also declared static so the same instance is available across Activity restarts, and uses the attach/detach pattern we first saw in Chapter 3, Distributing Work with Handler and HandlerThread, to ensure we don't leak references to the View hierarchy. We'll also create a static Messenger in our Activity that wraps our Handler.

private static PrimesHandler handler = new PrimesHandler();
private static Messenger messenger = new Messenger(handler);

@Override
protected void onStart() {
    super.onStart();
    handler.attach((TextView)findViewById(R.id.results));
}

@Override
protected void onStop() {
    super.onStop();
    handler.detach();
}

We can pass our Messenger to PrimesIntentService via an Intent that initiates a calculation.

Intent intent = new Intent(this, PrimesIntentService.class);
intent.putExtra(PrimesIntentService.PARAM, primeToFind);
intent.putExtra(PrimesIntentService.MSNGR, messenger);
startService(intent);

The onHandleIntent method of PrimesIntentService will need to extract the Messenger from the Intent and use it to send results back to the Activity. Sending messages with Messenger is very similar to sending them with Handler—we obtain a Message with the appropriate parameters and then send it with Messenger.send:

@Override
protected void onHandleIntent(Intent intent) {
  int primeToFind = intent.getIntExtra(PARAM, -1);
  Messenger messenger = intent.getParcelableExtra(MSNGR);
  try {
    if (primeToFind < 2) {
      messenger.send(Message.obtain(null, INVALID));
    } else {
      messenger.send(Message.obtain(
        null, RESULT, primeToFind, 0,
        calculateNthPrime(primeToFind)));
    }
  } catch (RemoteException anExc) {
    Log.e(TAG, "Unable to send message", anExc);
  }
}

In this section, we've started background work in a Service using Intents, and given the Service a communication channel it can use to send results back to the Activity, even if the Activity is restarted. In the next section, we'll look at some alternative ways to initiate and communicate with Services.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
44.220.184.63