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:
Service
Messenger
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.
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.
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);
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.
3.129.26.204