Chapter 4. Thread Communication

In multithreaded appplications, tasks can run in parallel and collaborate to produce a result. Hence, threads have to be able to communicate to enable true asynchronous processing. In Android, the importance of thread communication is emphasized in the platform-specific handler/looper mechanism that is the focus in this chapter, together with the traditional Java techniques. The chapter covers:

  • Passing data through a one-way data pipe
  • Shared memory communication
  • Implementing a consumer-producer pattern with BlockingQueue
  • Operations on message queues
  • Sending tasks back to the UI Thread

Pipes

Pipes are a part of the java.io package. That is, they are general Java functionality and not Android specific. A pipe provides a way for two threads, within the same process, to connect and establish a one-way data channel. A producer thread writes data to the pipe, whereas a consumer thread reads data from the pipe.

Note

The Java pipe is comparable to the Unix and Linux pipe operator (the | shell character) that is used to redirect the output from one command to the input for another command. The pipe operator works across processes in Linux, but Java pipes work across threads in the virtual machine, for example, within a process.

The pipe itself is a circular buffer allocated in memory, available only to the two connected threads. No other threads can access the data. Hence, thread safety—discussed in Thread Safety—is ensured. The pipe is also one-directional, permitting just one thread to write and the other to read (Figure 4-1).

Two threads connected through a pipe.
Figure 4-1. Thread communication with pipes

Pipes are typically used when you have two long-running tasks and one has to offload data to another continuously. Pipes make it easy to decouple tasks to several threads, instead of having only one thread handle many tasks. When one task has produced a result on a thread, it pipes the result on to the next thread that processes the data further. The gain comes from clean code separation and concurrent execution. Pipes can be used between worker threads and to offload work from the UI thread, which you want to keep light to preserve a responsive user experience.

A pipe can transfer either binary or character data. Binary data transfer is represented by PipedOutputStream (in the producer) and PipedInputStream (in the consumer), whereas character data transfer is represented by PipedWriter (in the producer) and PipedReader (in the consumer). Apart from the data transfer type, the two pipes have similar functionality. The lifetime of the pipe starts when either the writer or the reader thread establishes a connection, and it ends when the connection is closed.

Basic Pipe Use

The fundamental pipe life cycle can be summarized in three steps: setup, data transfer (which can be repeated as long as the two threads want to exchange data), and disconnection. The following examples are created with PipedWriter/PipedReader, but the same steps work with PipedOutputStream/PipedInputStream.

  1. Set up the connection:

    PipedReader r = new PipedReader();
    PipedWriter w = new PipedWriter();
    w.connect(r);

    Here, the connection is established by the writer connecting to the reader. The connection could just as well be established from the reader. Several constructors also implicitly set up a pipe. The default buffer size is 1024 but is configurable from the consumer side of the pipe, as shown later:

    int BUFFER_SIZE_IN_CHARS = 1024 * 4;
    PipedReader r = new PipedReader(BUFFER_SIZE_IN_CHARS);
    PipedWriter w = new PipedWriter(r);
  2. Pass the reader to a processing thread:

    Thread t = new MyReaderThread(r);
    t.start();

    After the reader thread starts, it is ready to receive data from the writer.

  3. Transfer data:

    // Producer thread: Write single character or array of characters
    w.write('A');
    
    // Consumer thread: Read the data
    int result = r.read();

    Communication adheres to the consumer-producer pattern with a blocking mechanism. If the pipe is full, the write() method will block until enough data has been read, and consequently removed from the pipe, to leave room for the data the writer is trying to add. The read() method blocks whenever there is no data to read from the pipe. It’s worth noticing that the read() method returns the character as an integer value to ensure that enough space is available to handle various encoding with different sizes. You can cast the integer value back to a character.

    In practice, a better approach would look like this:

    // Producer thread: Flush the pipe after a write.
    w.write('A');
    w.flush();
    
    // Consumer thread: Read the data in a loop.
    int i;
    while((i = reader.read()) != -1){
        char c = (char) i;
        // Handle received data
    }

    Calling flush() after a write to the pipe notifies the consumer thread that new data is available. This is useful from a performance perspective, because when the buffer is empty, the PipedReader uses a blocking call to wait() with one-second timeout. Hence, if the flush() call is omitted, the consumer thread may delay the reading of data up to one second. By calling flush(), the producer cuts short the wait in the consumer thread and allows data processing to continue immediately.

  4. Close the connection.

    When the communication phase is finished, the pipe should be disconnected:

    // Producer thread: Close the writer.
    w.close();
    
    // Consumer thread: Close the reader.
    r.close();

    If the writer and reader are connected, it’s enough to close only one of them. If the writer is closed, the pipe is disconnected but the data in the buffer can still be read. If the reader is closed, the buffer is cleared.

Example: Text Processing on a Worker Thread

This next example illustrates how pipes can process text that a user enters in an EditText. To keep the UI thread responsive, each character entered by the user is passed to a worker thread, which presumably handles some time-consuming processing:

 public class PipeExampleActivity extends Activity {

    private static final String TAG = "PipeExampleActivity";
    private EditText editText;

    PipedReader r;
    PipedWriter w;

    private Thread workerThread;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        r = new PipedReader();
        w = new PipedWriter();

        try {
            w.connect(r);
        } catch (IOException e) {
            e.printStackTrace();
        }

        setContentView(R.layout.activity_pipe);
        editText = (EditText) findViewById(R.id.edit_text);
        editText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int start,
                                          int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence charSequence, int start,
                                      int before, int count) {
                try {
                    // Only handle addition of characters
                    if(count > before) {
                        // Write the last entered character to the pipe
                        w.write(charSequence.subSequence(before, count).
                                toString());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void afterTextChanged(Editable editable) {
            }
        });

        workerThread = new Thread(new TextHandlerTask(r));
        workerThread.start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        workerThread.interrupt();
        try {
            r.close();
            w.close();
        } catch (IOException e) {
        }
    }

    private static class TextHandlerTask implements Runnable {
        private final PipedReader reader;

        public TextHandlerTask(PipedReader reader){
            this.reader = reader;
        }
        @Override
        public void run() {
            while(Thread.currentThread().isInterrupted()){
                try {
                    int i;
                    while((i = reader.read()) != -1){
                        char c = (char) i;
                        //ADD TEXT PROCESSING LOGIC HERE
                        Log.d(TAG, "char = " + c);
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

When the PipeExampleActivity is created, it will show an EditText box, which has a listener (TextWatcher) for changes in the content. Whenever a new character is added in the EditText, the character will be written to the pipe and read in the TextHandlerTask. The consumer task is an infinite loop that reads a character from the pipe as soon as there is anything to read. The inner while-loop will block when calling read() if the pipe is empty.

Warning

Be careful when involving the UI thread with pipes, due to the possible blocking of calls if the pipe is either full (producer blocks on its write() call) or empty (consumer blocks on its read() call).

Shared Memory

Shared memory (using the memory area known in programming as the heap) is a common way to pass information between threads. All threads in an application can access the same address space within the process. Hence, if one thread writes a value on a variable in the shared memory, it can be read by all the other threads, as shown in Figure 4-2.

Multiple threads accessing the same memory area.
Figure 4-2. Thread communication with shared memory

If a thread stores data as a local variable, no other thread can see it. By storing it in shared memory, it can use the variables for communication and share work with other threads. Objects are stored in the shared memory if they are scoped as one of the following:

  • Instance member variables
  • Class member variables
  • Objects declared in methods

The reference of an object is stored locally on the thread’s stack, but the object itself is stored in shared memory. The object is accessible from multiple threads only if the method publishes the reference outside the method scope, for example, by passing the reference to another object’s method. Threads communicate through shared memory by defining instance and class fields that are accessible from multiple threads.

Signaling

While threads are communicating through the state variables on the shared memory, they could poll the state value to fetch changes to the state. But a more efficient mechanism is the Java library’s built-in signaling mechanism that lets a thread notify other threads of changes in the state. The signaling mechanism varies depending on the synchronization type (see Table 4-1).

Table 4-1. Thread signaling
synchronized ReentrantLock ReentrantReadWriteLock

Blocking call, waiting for a state

Object.wait() Object.wait(timeout)

Condition.await() Condition.await(timeout)

Condition.await() Condition.await(timeout)

Signal blocked threads

Object.notify() Object.notifyAll()

Condition.signal() Condition.signalAll()

Condition.signal() Condition.signalAll()

When a thread cannot continue execution until another thread reaches a specific state, it calls wait()/wait(timeout) or the equivalents await()/await(timeout), depending on the synchronization used. The timeout parameters indicate how long the calling thread should wait before continuing the execution.

When another thread has changed the state, it signals the change with notify()/notifyAll() or the equivalents signal()/signalAll(). Upon a signal, the waiting thread continues execution. The calls thus support two different design patterns that use conditions: the notify() or signal() version wakes one thread, chosen at random, whereas the notifyAll() or signalAll() version wakes all threads waiting on the signal.

Because multiple threads could receive the signal and one could enter the critical section before the others wake, receiving the signal does not guarantee that the correct state is achieved. A waiting thread should apply a design pattern where it checks that the wanted condition is fulfilled before executing further. For example, if the shared state is protected with synchronization on the intrinsic lock, check the condition before calling wait():

synchronized(this) {
    while(isConditionFulfilled == false) {
        wait();
    }
    // When the execution reaches this point,
    // the state is correct.
}

This pattern checks whether the condition predicate is fulfilled. If not, the thread blocks by calling wait(). When another thread notifies on the monitor and the waiting thread wakes up, it checks again whether the condition has been fulfilled and, if not, it blocks again, waiting for a new signal.

Warning

A very common Android use case is to create a worker thread from the UI thread and let the worker thread produce a result to be used by some UI element, so the UI thread should wait for the result. However, the UI thread should not wait for a signal from a background thread, as it may block the UI thread. Instead, use the Android message passing mechanism discussed later.

BlockingQueue

Thread signaling is a low-level, highly configurable mechanism that can be adapted to fit many use cases, but it may also be considered as the most error-prone technique. Therefore, the Java platform builds high-level abstractions upon the thread signaling mechanism to solve one-directional handoff of arbitrary objects between threads. The abstraction is often called “solving the producer-consumer synchronization problem.” The problem consists of use cases where there can be threads producing content (producer threads) and threads consuming content (consumer threads). The producers hand off messages for the consumers to process. The intermediator between the threads is a queue with blocking behavior, i.e., java.util.concurrent.BlockingQueue (see Figure 4-3).

Producer and consumer threads communicating via a BlockingQueue.
Figure 4-3. Thread communication with BlockingQueue

The BlockingQueue acts as the coordinator between the producer and consumer threads, wrapping a list implementation together with thread signaling. The list contains a configurable number of elements that the producing threads fill with arbitrary data messages. On the other side, the consumer threads extract the messages in the order that they were enqueued and then process them. Coordination between the producers and consumers is necessary if they get out of sync, for example, if the producers hand off more messages than the consumers can handle. So BlockingQueue uses thread conditions to ensure that producers cannot enqueue new messages if the BlockingQueue list is full, and that consumers know when there are messages to fetch. Synchronization between the threads can be achieved with thread signaling, as Example: Consumer and Producer shows. But the BlockingQueue both blocks threads and signals the important state changes—i.e., the list is not full and the list is not empty.

The consumer-producer pattern implemented with the LinkedBlockingQueue-implementation is easily implemented by adding messages to the queue with put(), and removing them with take(), where put() blocks the caller if the queue is full, and take() blocks the caller if the queue is empty:

public class ConsumerProducer {

    private final int LIMIT = 10;
    private BlockingQueue<Integer> blockingQueue =
       new LinkedBlockingQueue<Integer>(LIMIT);

    public void produce() throws InterruptedException {
        int value = 0;

        while (true) {
            blockingQueue.put(value++);
        }
    }

    public void consume() throws InterruptedException {
        while (true) {
            int value = blockingQueue.take();
        }
    }
}

Android Message Passing

So far, the thread communication options discussed have been regular Java, available in any Java application. The mechanisms—pipes, shared memory, and blocking queues—apply to Android applications but impose problems for the UI thread because of their tendency to block. The UI thread responsiveness is at risk when using mechanisms with blocking behavior, because that may occasionally hang the thread.

The most common thread communication use case in Android is between the UI thread and worker threads. Hence, the Android platform defines its own message passing mechanism for communication between threads. The UI thread can offload long tasks by sending data messages to be processed on background threads. The message passing mechanism is a nonblocking consumer-producer pattern, where neither the producer thread nor the consumer thread will block during the message handoff.

The message handling mechanism is fundamental in the Android platform and the API is located in the android.os package, with a set of classes shown in Figure 4-4 that implement the functionality.

Overview of the Android message passing components.
Figure 4-4. API overview
android.os.Looper
A message dispatcher associated with the one and only consumer thread.
android.os.Handler
Consumer thread message processor, and the interface for a producer thread to insert messages into the queue. A Looper can have many associated handlers, but they all insert messages into the same queue.
android.os.MessageQueue
Unbounded linked list of messages to be processed on the consumer thread. Every Looper—and Thread—has at most one MessageQueue.
android.os.Message
Message to be executed on the consumer thread.

Messages are inserted by producer threads and processed by the consumer thread, as illustrated in Figure 4-5.

  1. Insert: The producer thread inserts messages in the queue by using the Handler connected to the consumer thread, as shown in Handler.
  2. Retrieve: The Looper, discussed in Looper, runs in the consumer thread and retrieves messages from the queue in a sequential order.
  3. Dispatch: The handlers are responsible for processing the messages on the consumer thread. A thread may have multiple Handler instances for processing messages; the Looper ensures that messages are dispatched to the correct Handler.
Producer threads and consumer thread communicating via a MessageQueue.
Figure 4-5. Overview of the message-passing mechanism between multiple producer threads and one consumer thread. Every message refers to to the next message in the queue, here indicated by a left-pointing arrow.

Example: Basic Message Passing

Before we dissect the components in detail, let’s look at a fundamental message passing example to get us acquainted with the code setup.

The following code implements what is probably one of the most common use cases. The user presses a button on the screen that could trigger a long operation, such as a network operation. To avoid stalling the rendering of the UI, the long operation, represented here by a dummy doLongRunningOperation() method, has to be executed on a worker thread. Hence, the setup is merely one producer thread (the UI thread) and one consumer thread (LooperThread).

Our code sets up a message queue. It handles the button click as usual in the onClick() callback, which executes on the UI thread. In our implementation, the callback inserts a dummy message into the message queue. For sake of brevity, layouts and UI components have been left out of the example code:

public class LooperActivity extends Activity {

    LooperThread mLooperThread;

    private static class LooperThread extends Thread { 1

        public Handler mHandler;

        public void run() {
            Looper.prepare(); 2
            mHandler = new Handler() { 3
                public void handleMessage(Message msg) { 4
                    if(msg.what == 0) {
                        doLongRunningOperation();
                    }
                }
            };
            Looper.loop(); 5
            }
    }

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mLooperThread = new LooperThread(); 6
        mLooperThread.start();
    }

    public void onClick(View v) {
        if (mLooperThread.mHandler != null) {7
            Message msg = mLooperThread.mHandler.obtainMessage(0); 8
                        mLooperThread.mHandler.sendMessage(msg); 9
        }
    }

    private void doLongRunningOperation() {
        // Add long running operation here.
    }

    protected void onDestroy() {
        mLooperThread.mHandler.getLooper().quit(); 10
    }
}
1

Definition of the worker thread, acting as a consumer of the message queue.

2

Associate a Looper—and implicitly a MessageQueue—with the thread.

3

Set up a Handler to be used by the producer for inserting messages in the queue. Here we use the default constructor so it will bind to the Looper of the current thread. Hence, this Handler can created only after Looper.prepare(), or it will have nothing to bind to.

4

Callback that runs when the message has been dispatched to the worker thread. It checks the what parameter and then executes the long operation.

5

Start dispatching messages from the message queue to the consumer thread. This is a blocking call, so the worker thread will not finish.

6

Start the worker thread, so that it is ready to process messages.

7

There is race condition between the setup of mHandler on a background thread and this usage on the UI thread. Hence, validate that mHandler is available.

8

Initialize a Message-object with the what argument arbitrarily set to 0.

9

Insert the message in the queue.

10

Terminate the background thread. The call to Looper.quit() stops the dispatching of messages and releases Looper.loop() from blocking so the run method can finish, leading to the termination of the thread.

Classes Used in Message Passing

Let’s take a more detailed look now at the specific components of message passing and their use.

MessageQueue

The message queue is represented by the android.os.MessageQueue class. It is built with linked messages, constituting an unbound one-directional linked list. Producer threads insert messages that will later be dispatched to the consumer. The messages are sorted based on timestamps. The pending message with the lowest timestamp value is first in line for dispatch to the consumer. However, a message is dispatched only if the timestamp value is less than the current time. If not, the dispatch will wait until the current time has passed the timestamp value.

Figure 4-6 illustrates a message queue with three pending messages, sorted with timestamps where t1 < t2 < t3. Only one message has passed the dispatch barrier, which is the current time. Messages eligible for dispatch have a timestamp value less than the current time (represented by “Now” in the figure).

MessageQueue with three pending messages.
Figure 4-6. Pending messages in the queue. The rightmost message is first in queue to be processed. The message arrows denote references to the next message in the queue.

If no message has passed the dispatch barrier when the Looper is ready to retrieve the next message, the consumer thread blocks. Execution is resumed as soon as a message passes the dispatch barrier.

The producers can insert new messages in the queue at any time and on any position in the queue. The insert position in the queue is based on the timestamp value. If a new message has the lowest timestamp value compared to the pending messages in the queue, it will occupy the first position in the queue, which is next to be dispatched. Insertions always conform to the timestamp sorting order. Message insertion is discussed further in Handler.

MessageQueue.IdleHandler

If there is no message to process, a consumer thread has some idle time. For instance, Figure 4-7 illustrates a time slot where the consumer thread is idle. By default, the consumer thread simply waits for new messages during idle time; but instead of waiting, the thread can be utilized to execute other tasks during these idle slots. This feature can be utilized to let noncritical tasks postpone their execution until no other messages are competing for execution time.

Idle time slot.
Figure 4-7. If no message has passed the dispatch barrier, there is a time slot that can be utilized for execution before the next pending message needs to be executed

When a pending message has been dispatched, and no other message has passed the dispatch barrier, a time slot occurs where the consumer thread can be utilized for execution of other tasks. An application gets hold of this time slot with the android.os.MessageQueue.IdleHandler-interface, a listener that generates callbacks when the consumer thread is idle. The listener is attached to the MessageQueue and detached from it through the following calls:

// Get the message queue of the current thread.
MessageQueue mq = Looper.myQueue();
// Create and register an idle listener.
MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler();
mq.addIdleHandler(idleHandler)
// Unregister an idle listener.
mq.removeIdleHandler(idleHandler)

The idle handler interface consists of one callback method only:

interface IdleHandler {
    boolean queueIdle();
}

When the message queue detects idle time for the consumer thread, it invokes queueIdle() on all registered IdleHandler-instances. It is up to the application to implement the callback responsibly. You should usually avoid long-running tasks because they will delay pending messages during the time they run.

The implementation of queueIdle() must return a Boolean value with the following meanings:

true
The idle handler is kept active; it will continue to receive callbacks for successive idle time slots.
false
The idle handler is inactive; it will not receive anymore callbacks for successive idle time slots. This is the same thing as removing the listener through MessageQueue.removeIdleHandler().

Example: Using IdleHandler to terminate an unused thread

All registered IdleHandlers to a MessageQueue are invoked when a thread has idle slots, where it waits for new messages to process. The idle slots can occur before the first message, between messages, and after the last message. If multiple content producers should process data sequentially on a consumer thread, the IdleHandler can be used to terminate the consumer thread when all messages are processed so that the unused thread does not linger in memory. With the IdleHandler, it is not necessary to keep track of the last inserted message to know when the thread can be terminated.

Warning

This use case applies only when the producing threads insert messages in the MessageQueue without delay, so that the consumer thread is never idle until the last message is inserted.

The ConsumeAndQuitThread method shows the structure of a consuming thread with Looper and MessageQueue that terminates the thread when there are no more messages to process:

public class ConsumeAndQuitThread extends Thread
    implements MessageQueue.IdleHandler {

    private static final String THREAD_NAME = "ConsumeAndQuitThread";

    public Handler mConsumerHandler;
    private boolean mIsFirstIdle = true;

    public ConsumeAndQuitThread() {
        super(THREAD_NAME);
    }

    @Override
    public void run() {
        Looper.prepare();
        mConsumerHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                    // Consume data
            }
        };
        Looper.myQueue().addIdleHandler(this);1
        Looper.loop();
    }


    @Override
    public boolean queueIdle() {
        if (mIsFirstIdle) { 2
            mIsFirstIdle = false;
            return true; 3
        }
        mConsumerHandler.getLooper().quit(); 4
        return false;
    }

    public void enqueueData(int i) {
        mConsumerHandler.sendEmptyMessage(i);
    }
}
1

Register the IdleHandler on the background thread when it is started and the Looper is prepared so that the MessageQueue is set up.

2

Let the first queueIdle invocation pass, since it occurs before the first message is received.

3

Return true on the first invocation so that the IdleHandler still is registered.

4

Terminate the thread.

The message insertion is done from multiple threads concurrently, with a simulated randomness of the insertion time:

final ConsumeAndQuitThread consumeAndQuitThread = new ConsumeAndQuitThread();
consumeAndQuitThread.start();

for (int i = 0; i < 10; i++) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                SystemClock.sleep(new Random().nextInt(10));
                consumeAndQuitThread.enqueueData(i);
            }
        }
    }).start();

Message

Each item on the MessageQueue is of the android.os.Message class. This is a container object carrying either a data item or a task, never both. Data is processed by the consumer thread, whereas a task is simply executed when it is dequeued and you have no other processing to do:

Note

The message knows its recipient processor—i.e., Handler—and can enqueue itself through Message.sendToTarget():

Message m = Message.obtain(handler, runnable);
m.sendToTarget();

As we will see in Handler, the handler is most commonly used for message enqueuing, as it offers more flexibility with regard to message insertion.

Data message

The data set has multiple parameters that can be handed off to the consumer thread, as shown in Table 4-2.

Table 4-2. Message parameters
Parameter name Type Usage

what

int

Message identifier. Communicates intention of the message.

arg1, arg2

int

Simple data values to handle the common use case of handing over integers. If a maximum of two integer values are to be passed to the consumer, these parameters are more efficient than allocating a Bundle, as explained under the data parameter.

obj

Object

Arbitrary object. If the object is handed off to a thread in another process, it has to implement Parcelable.

data

Bundle

Container of arbitrary data values.

replyTo

Messenger

Reference to Handler in some other process. Enables interprocess message communication, as described in Two-Way Communication.

callback

Runnable

Task to execute on a thread. This is an internal instance field that holds the Runnable object from the Handler.post methods in Handler.

Task message
The task is represented by a java.lang.Runnable object to be executed on the consumer thread. Task messages cannot contain any data beyond the task itself.

A MessageQueue can contain any combination of data and task messages. The consumer thread processes them in a sequential manner, independent of the type. If a message is a data message, the consumer processes the data. Task messages are handled by letting the Runnable execute on the consumer thread, but the consumer thread does not receive a message to be processed in Handler.handleMessage(Message), as it does with data messages.

The lifecycle of a message is simple: the producer creates the message, and eventually it is processed by the consumer. This description suffices for most use cases, but when a problem arises, a deeper understanding of message handling is invaluable. Let us take a look into what actually happens with the message during its lifecycle, which can be split up into four principal states shown in Figure 4-8. The runtime stores message objects in an application-wide pool to enable the reuse of previous messages; this avoids the overhead of creating new instances for every handoff. The message object execution time is normally very short, and many messages are processed per time unit.

Message lifecycle states.
Figure 4-8. Message lifecycle states

The state transfers are partly controlled by the application and partly by the platform. Note that the states are not observable, and an application cannot follow the changes from one state to another (although there are ways to follow the movement of messages, explained in Observing the Message Queue). Therefore, an application should not make any assumptions about the current state when handling a message.

Initialized

In the initialized state, a message object with mutable state has been created and, if it is a data message, populated with data. The application is responsible for creating the message object using one of the following calls. They take an object from the object pool:

  • Explicit object construction:

    Message m  = new Message();
  • Factory methods:

    • Empty message:

      Message m = Message.obtain();
    • Data message:

      Message m = Message.obtain(Handler h);
      Message m = Message.obtain(Handler h, int what);
      Message m = Message.obtain(Handler h, int what, Object o);
      Message m = Message.obtain(Handler h, int what, int arg1, int arg2);
      Message m = Message.obtain(Handler h, int what, int arg1, int arg2,
                                 Object o);
    • Task message:

      Message m = Message.obtain(Handler h, Runnable task);
    • Copy constructor:

      Message m = Message.obtain(Message originalMsg);

Pending

The message has been inserted into the queue by the producer thread, and it is waiting to be dispatched to the consumer thread.

Dispatched

In this state, the Looper has retrieved and removed the message from the queue. The message has been dispatched to the consumer thread and is currently being processed. There is no application API for this operation because the dispatch is controlled by the Looper , without the influence of the application. When the Looper dispatches a message, it checks the delivery information of the message and delivers the message to the correct recipient. Once dispatched, the message is executed on the consumer thread.

Recycled

At this point in the lifecycle, the message state is cleared and the instance is returned to the message pool. The Looper handles the recycling of the message when it has finished executing on the consumer thread. Recycling of messages is handled by the runtime and should not be done explicitly by the application.

Note

Once a message is inserted in the queue, the content should not be altered. In theory, it is valid to change the content before the message is dispatched. However, because the state is not observable, the message may be processed by the consumer thread while the producer tries to change the data, raising thread safety concerns. It would be even worse if the message has been recycled, because it then has been returned to the message pool and possibly used by another producer to pass data in another queue.

Looper

The android.os.Looper class handles the dispatch of messages in the queue to the associated handler. All messages that have passed the dispatch barrier, as illustrated in Figure 4-6, are eligible for dispatch by the Looper. As long as the queue has messages eligible for dispatch, the Looper will ensure that the consumer thread receives the messages. When no messages have passed the dispatch barrier, the consumer thread will block until a message has passed the dispatch barrier.

The consumer thread does not interact with the message queue directly to retrieve the messages. Instead, a message queue is added to the thread when the Looper has been attached. The Looper manages the message queue and facilitates the dispatch of messages to the consumer thread.

By default, only the UI thread has a Looper; threads created in the application need to get a Looper associated explicitly. When the Looper is created for a thread, it is connected to a message queue. The Looper acts as the intermediator between the queue and the thread. The setup is done in the run method of the thread:

class ConsumerThread extends Thread {
    @Override
    public void run() {
        Looper.prepare(); 1

        // Handler creation omitted.

        Looper.loop(); 2
    }
}
1

The first step is to create the Looper, which is done with the static prepare() method; it will create a message queue and associate it with the current thread. At this point, the message queue is ready for insertion of messages, but they are not dispatched to the consumer thread.

2

Start handling messages in the message queue. This is a blocking method that ensures the run() method is not finished; while run() blocks, the Looper dispatches messages to the consumer thread for processing.

A thread can have only one associated Looper; a runtime error will occur if the application tries to set up a second one. Consequently, a thread can have only one message queue, meaning that messages sent by multiple producer threads are processed sequentially on the consumer thread. Hence, the currently executing message will postpone subsequent messages until it has been processed. Messages with long execution times shall not be used if they can delay other important tasks in the queue.

Looper termination

The Looper is requested to stop processing messages with either quit or quitSafely: quit() stops the looper from dispatching any more messages from the queue; all pending messages in the queue, including those that have passed the dispatch barrier, will be discarded. quitSafely, on the other hand, only discards the messages that have not passed the dispatch barrier. Pending messages that are eligible for dispatch will be processed before the Looper is terminated.

Note

quitSafely was added in API level 18 (Jelly Bean 4.3). Previous API levels only support quit.

Terminating a Looper does not terminate the thread; it merely exits Looper.loop() and lets the thread resume running in the method that invoked the loop call. But you cannot start the old Looper or a new one, so the thread can no longer enqueue or handle messages. If you call Looper.prepare(), it will throw RuntimeException because the thread already has an attached Looper. If you call Looper.loop(), it will block, but no messages will be dispatched from the queue.

The UI thread Looper

The UI thread is the only thread with an associated Looper by default. It is a regular thread, like any other thread created by the application itself, but the Looper is associated with the thread[7] before the application components are initialized.

There are a few practical differences between the UI thread Looper and other application thread loopers:

  • It is accessible from everywhere, through the Looper.getMainLooper() method.
  • It cannot be terminated. Looper.quit() throws RuntimeException.
  • The runtime associates a Looper to the UI thread by Looper.prepareMainLooper(). This can be done only once per application. Thus, trying to attach the main looper to another thread will throw an exception.

Handler

So far, the focus has been on the internals of Android thread communication, but an application mostly interacts with the android.os.Handler class. It is a two-sided API that both handles the insertion of messages into the queue and the message processing. As indicated in Figure 4-5, it is invoked from both the producer and consumer thread typically used for:

  • Creating messages
  • Inserting messages into the queue
  • Processing messages on the consumer thread
  • Managing messages in the queue

Setup

While carrying out its responsibilities, the Handler interacts with the Looper, message queue, and message. As Figure 4-4 illustrates, the only direct instance relation is to the Looper, which is used to connect to the MessageQueue. Without a Looper, handlers cannot function; they cannot couple with a queue to insert messages, and consequently they will not receive any messages to process. Hence, a Handler instance is already bound to a Looper instance at construction time:

  • Constructors without an explicit Looper bind to the Looper of the current thread:

    new Handler();
    new Handler(Handler.Callback)
  • Constructors with an explicit Looper bind to that Looper:

    new Handler(Looper);
    new Handler(Looper, Handler.Callback);

If the constructors without an explicit Looper are called on a thread without a Looper (i.e., it has not called Looper.prepare()), there is nothing handlers can bind to, leading to a RuntimeException. Once a handler is bound to a Looper, the binding is final.

A thread can have multiple handlers; messages from them coexist in the queue but are dispatched to the correct Handler instance, as shown in Figure 4-9.

Multiple handlers.
Figure 4-9. Multiple handlers using one Looper. The handler inserting a message is the same handler that processes the message.

Note

Multiple handlers will not enable concurrent execution. The messages are still in the same queue and are processed sequentially.

Message creation

For simplicity, the Handler class offers wrapper functions for the factory methods shown in Initialized to create objects of the Message class:

Message obtainMessage(int what, int arg1, int arg2)
Message obtainMessage()
Message obtainMessage(int what, int arg1, int arg2, Object obj)
Message obtainMessage(int what)
Message obtainMessage(int what, Object obj)

The message obtained from a Handler is retrieved from the message pool and implicitly connected to the Handler instance that requested it. This connection enables the Looper to dispatch each message to the correct Handler.

Message insertion

The Handler inserts messages in the message queue in various ways depending on the message type. Task messages are inserted through methods that are prefixed post, whereas data insertion methods are prefixed send:

  • Add a task to the message queue:

    boolean post(Runnable r)f
    boolean postAtFrontOfQueue(Runnable r)
    boolean postAtTime(Runnable r, Object token, long uptimeMillis)
    boolean postAtTime(Runnable r, long uptimeMillis)
    boolean postDelayed(Runnable r, long delayMillis)
  • Add a data object to the message queue:

    boolean sendMessage(Message msg)
    boolean sendMessageAtFrontOfQueue(Message msg)
    boolean sendMessageAtTime(Message msg, long uptimeMillis)
    boolean sendMessageDelayed(Message msg, long delayMillis)
  • Add simple data object to the message queue:

    boolean sendEmptyMessage(int what)
    boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
    boolean sendEmptyMessageDelayed(int what, long delayMillis)

All insertion methods put a new Message object in the queue, even though the application does not create the Message object explicitly. The objects, such as Runnable in a task post and what in a send, are wrapped into Message objects, because those are the only data types allowed in the queue.

Every message inserted in the queue comes with a time parameter indicating the time when the message is eligible for dispatch to the consumer thread. The sorting is based on the time parameter, and it is the only way an application can affect the dispatch order:

default
Immediately eligible for dispatch.
at_front
This message is eligible for dispatch at time 0. Hence, it will be the next dispatched message, unless another is inserted at the front before this one is processed.
delay
The amount of time after which this message is eligible for dispatch.
uptime
The absolute time at which this message is eligible for dispatch.

Even though explicit delays or uptimes can be specified, the time required to process each message is still indeterminate. It depends both on whatever existing messages need to be processed first and the operating system scheduling.

Inserting a message in the queue is not failsafe. Some common errors that can occur are listed in Table 4-3.

Table 4-3. Message insertion errors
Failure Error response Typical application problem

Message has no Handler.

RuntimeException

Message was created from a Message.obtain() method without a specified Handler.

Message has already been dispatched and is being processed.

RuntimeException

The same message instance was inserted twice.

Looper has exited.

Return false

Message is inserted after Looper.quit() has been called.

Warning

The dispatchMessage method of the Handler class is used by the Looper to dispatch messages to the consumer thread. If used by the application directly, the message will be processed immediately on the calling thread and not the consumer thread.

Example: Two-way message passing

The HandlerExampleActivity simulates a long-running operation that is started when the user clicks a button. The long-running task is executed on a background thread; meanwhile, the UI displays a progress bar that is removed when the background thread reports the result back to the UI thread.

First, the setup of the Activity:

public class HandlerExampleActivity extends Activity {

    private final static int SHOW_PROGRESS_BAR = 1;
    private final static int HIDE_PROGRESS_BAR = 0;
    private BackgroundThread mBackgroundThread;

    private TextView mText;
    private Button mButton;
    private ProgressBar mProgressBar;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_example);

        mBackgroundThread = new BackgroundThread();
        mBackgroundThread.start();1

        mText = (TextView) findViewById(R.id.text);
        mProgressBar = (ProgressBar) findViewById(R.id.progress);
        mButton = (Button) findViewById(R.id.button);
        mButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mBackgroundThread.doWork(); 2
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mBackgroundThread.exit();3
    }

    // ... The rest of the Activity is defined further down

}
1

A background thread with a message queue is started when the HandlerExampleActivity is created. It handles tasks from the UI thread.

2

When the user clicks a button, a new task is sent to the background thread. As the tasks will be executed sequentially on the background thread, multiple button clicks may lead to queueing of tasks before they are processed.

3

The background thread is stopped when the HandlerExampleActivity is destroyed.

BackgroundThread is used to offload tasks from the UI thread. It runs—and can receive messages—during the lifetime of the HandlerExampleActivity. It does not expose its internal Handler; instead it wraps all accesses to the Handler in public methods doWork and exit:

private class BackgroundThread extends Thread {

    private Handler mBackgroundHandler;

    public void run() { 1
        Looper.prepare();
        mBackgroundHandler = new Handler(); 2
        Looper.loop();
    }

    public void doWork() {
        mBackgroundHandler.post(new Runnable() { 3
            @Override
            public void run() {
                Message uiMsg = mUiHandler.obtainMessage(
                    SHOW_PROGRESS_BAR, 0, 0, null); 4

                mUiHandler.sendMessage(uiMsg); 5

                Random r = new Random();
                int randomInt = r.nextInt(5000);
                SystemClock.sleep(randomInt); 6

                uiMsg = mUiHandler.obtainMessage(
                    HIDE_PROGRESS_BAR, randomInt, 0, null); 7
                    mUiHandler.sendMessage(uiMsg); 8
            }
        });
    }

    public void exit() { 9
        mBackgroundHandler.getLooper().quit();
    }
}
1

Associate a Looper with the thread.

2

The Handler processes only Runnables. Hence, it is not required to implement Handler.handleMessage.

3

Post a long task to be executed in the background.

4

Create a Message object that contains only a what argument with a command—SHOW_PROGRESS_BAR—to the UI thread so that it can show the progress bar.

5

Send the start message to the UI thread.

6

Simulate a long task of random length, that produces some data randomInt.

7

Create a Message object with the result randomInt, that is passed in the arg1 parameter. The what parameter contains a command—HIDE_PROGRESS_BAR—to remove the progress bar.

8

The message with the end result that both informs the UI thread that the task is finished and delivers a result.

9

Quit the Looper so that the thread can finish.

The UI thread defines its own Handler that can receive commands to control the progress bar and update the UI with results from the background thread:

private final Handler mUiHandler = new Handler() {
    public void handleMessage(Message msg) {

        switch(msg.what) {
            case SHOW_PROGRESS_BAR: 1
                mProgressBar.setVisibility(View.VISIBLE);
                break;
            case HIDE_PROGRESS_BAR: 2
                mText.setText(String.valueOf(msg.arg1));
                mProgressBar.setVisibility(View.INVISIBLE);
                break;
        }
    }
};
1

Show the progress bar.

2

Hide the progress bar and update the TextView with the produced result.

Message processing

Messages dispatched by the Looper are processed by the Handler on the consumer thread. The message type determines the processing:

Task messages
Task messages contain only a Runnable and no data. Hence, the processing to be executed is defined in the run method of the Runnable, which is executed automatically on the consumer thread, without invoking Handler.handleMessage().
Data messages
When the message contains data, the Handler is the receiver of the data and is responsible for its processing. The consumer thread processes the data by overriding the Handler.handleMessage(Message msg) method. There are two ways to do this, described in the text that follows.

One way to define handleMessage is to do it as part of creating a Handler. The method should be defined as soon as the message queue is available (after Looper.prepare() is called) but before the message retrieval starts (before Looper.loop() is called).

A template follows for setting up the handling of data messages:

class ConsumerThread extends Thread {
    Handler mHandler;
    @Override
    public void run() {
        Looper.prepare();
        mHandler = new Handler() {
            public void handleMessage(Message msg) {
                // Process data message here
            }
        };)
        Looper.loop();
    }
}

In this code, the Handler is defined as an anonymous inner class, but it could as well have been defined as a regular or inner class.

A convenient alternative to extending the Handler class is to use the Handler.Callback interface, which defines a handleMessage method with an additional return parameter not in Handler.handleMessage():

public interface Callback {
    public boolean handleMessage(Message msg);
}

With the Callback interface, it is not necessary to extend the Handler class. Instead, the Callback implementation can be passed to the Handler constructor, and it will then receive the dispatched messages for processing:

public class HandlerCallbackActivity extends Activity implements Handler.Callback {
    Handler mUiHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mUiHandler = new Handler(this);
    }

    @Override
    public boolean handleMessage(Message message) {
        // Process messages
        return true;
    }
}

Callback.handleMessage should return true if the message is handled, which guarantees that no further processing of the message is done. If, however, false is returned, the message is passed on to the Handler.handleMessage method for further processing. Note that the Callback does not override Handler.handleMessage. Instead, it adds a message preprocessor that is invoked before the Handlers own method. The Callback preprocessor can intercept and change messages before the Handler receives them. The following code shows the principle for intercepting messages with the Callback:

public class HandlerCallbackActivity extends Activity implements Handler.Callback {1

    @Override
    public boolean handleMessage(Message msg) { 2
        switch (msg.what) {
            case 1:
                msg.what = 11;
                return true;
            default:
                msg.what = 22;
                return false;
        }
    }

    // Invoked on button click
    public void onHandlerCallback(View v) {
        Handler handler = new Handler(this) {
            @Override
            public void handleMessage(Message msg) {
                // Process message 3
            }
        };
        handler.sendEmptyMessage(1); 4
        handler.sendEmptyMessage(2); 5
    }
}
1

The HandlerCallbackActivity implements the Callback interface to intercept messages.

2

The Callback implementation intercepts messages. If msg.what is 1, it returns true—the message is handled. Otherwise, it changes the value of msg.what to 22 and returns false—the message is not handled, so it is passed on to the Handler implementation of handleMessage.

3

Process messages in the second Handler.

4

Insert a message with msg.what == 1. The message is intercepted by the Callback as it returns true.

5

Insert a message with msg.what == 2. The message is changed by the Callback and passed on to the Handler that prints Secondary Handler - msg = 22.

Removing Messages from the Queue

After enqueuing a message, the producer can invoke a method of the Handler class to remove the message, so long as it has not been dequeued by the Looper. Sometimes an application may want to clean the message queue by removing all messages, which is possible, but most often a more fine-grained approach is desired: an application wants to target only a subset of the messages. For that, it needs to be able to identify the correct messages. Therefore, messages can be identified from certain properties, as shown in Table 4-4.

Table 4-4. Message identifiers
Identifier type Description Messages to which it applies

Handler

Message receiver

Both task and data messages

Object

Message tag

Both task and data messages

Integer

what parameter of message

Data messages

Runnable

Task to be executed

Task messages

The handler identifier is mandatory for every message, because a message always knows what Handler it will be dispatched to. This requirement implicitly restricts each Handler to removing only messages belonging to that Handler. It is not possible for a Handler to remove messages in the queue that were inserted by another Handler.

The methods available in the Handler class for managing the message queue are:

  • Remove a task from the message: queue.

    removeCallbacks(Runnable r)
    removeCallbacks(Runnable r, Object token)
  • Remove a data message from the message queue:

    removeMessages(int what)
    removeMessages(int what, Object object)
  • Remove tasks and data messages from the message queue:

    removeCallbacksAndMessages(Object token)

The Object identifier is used in both the data and task message. Hence, it can be assigned to messages as a kind of tag, allowing you later to remove related messages that you have tagged with the same Object.

For instance, the following excerpt inserts two messages in the queue to make it possible to remove them later based on the tag:

Object tag = new Object(); 1

Handler handler = new Handler()
    public void handleMessage(Message msg) {
        // Process message
        Log.d("Example", "Processing message");
    }
};

Message message = handler.obtainMessage(0, tag); 2
handler.sendMessage(message);

handler.postAtTime(new Runnable() { 3
    public void run() {
        // Left empty for brevity
    }
}, tag, SystemClock.uptimeMillis());

handler.removeCallbacksAndMessages(tag); 4
1

The message tag identifier, common to both the task and data message.

2

The object in a Message instance is used both as data container and implicitly defined message tag.

3

Post a task message with an explicitly defined message tag.

4

Remove all messages with the tag.

As indicated before, you have no way to find out whether a message was dispatched and handled before you issue a call to remove it. Once the message is dispatched, the producer thread that enqueued it cannot stop its task from executing or its data from being processed.

Observing the Message Queue

It is possible to observe pending messages and the dispatching of messages from a Looper to the associated handlers. The Android platform offers two observing mechanisms. Let us take a look at them by example.

The first example shows how it is possible to log the current snapshot of pending messages in the queue.

Taking a snapshot of the current message queue

This example creates a worker thread when the Activity is created. When the user presses a button, causing onClick to be called, six messages are added to the queue in different ways. Afterward we observe the state of the message queue:

public class MQDebugActivity extends Activity {

    private static final String TAG = "EAT";
    Handler mWorkerHandler;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mqdebug);

        Thread t = new Thread() {
            @Override
            public void run() {
                Looper.prepare();
                mWorkerHandler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        Log.d(TAG, "handleMessage - what = " + msg.what);
                    }
                };
                Looper.loop();
            }
        };
        t.start();
    }

    // Called on button click, i.e. from the UI thread.
    public void onClick(View v) {
        mWorkerHandler.sendEmptyMessageDelayed(1, 2000);
        mWorkerHandler.sendEmptyMessage(2);
        mWorkerHandler.obtainMessage(3, 0, 0, new Object()).sendToTarget();
        mWorkerHandler.sendEmptyMessageDelayed(4, 300);
        mWorkerHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "Execute");
            }
        }, 400);
        mWorkerHandler.sendEmptyMessage(5);

        mWorkerHandler.dump(new LogPrinter(Log.DEBUG, TAG), "");
    }
}

Six messages, with the parameters shown in Figure 4-10, are added to the queue.

Message queue example.
Figure 4-10. Added messages in the queue

Right after the messages are added to the queue, a snapshot is printed to the log. Only pending messages are observed. Hence, the number of messages actually observed depends on how many messages have already been dispatched to the handler. Three of the messages are added without a delay, which makes them eligible for dispatch at the time of the snapshot.

A typical run of the preceding code produces the following log:

49.397: handleMessage - what = 2
49.397: handleMessage - what = 3
49.397: handleMessage - what = 5
49.397: Handler (com.eat.MQDebugActivity$1$1) {412cb3d8} @ 5994288
49.407: Looper{412cb070}
49.407:     mRun=true
49.407:     mThread=Thread[Thread-111,5,main]
49.407:     mQueue=android.os.MessageQueue@412cb090
49.407:         Message 0: { what=4 when=+293ms }
49.407:         Message 1: { what=0 when=+394ms }
49.407:         Message 2: { what=1 when=+1s990ms }
49.407:         (Total messages: 3)
49.707: handleMessage - what = 4
49.808: Execute
51.407: handleMessage - what = 1

The snapshot of the message queue shows that the messages with what parameters (0, 1, and 4) are pending in the queue. These are the messages added to the queue with a dispatch delay, whereas the others without a dispatch delay apparently have been dispatched already. This is a reasonable result because the handler processing is very short—just a print to the log.

The snapshot also shows how much time is left before each message in the queue will pass the dispatch barrier. For instance, the next message to pass the barrier is Message 0 (what= 4) in 293 ms. Messages still pending in the queue but eligible for dispatch will have a negative time indication in the log—e.g., if when is less than zero.

Tracing the message queue processing

The message processing information can be printed to the log. Message queue logging is enabled from the Looper class. The following call enables logging on the message queue of the calling thread:

Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, TAG));

Let’s look at an example of tracing a message that is posted to the UI thread:

mHandler.post(new Runnable() {
    @Override
    public void run() {
        Log.d(TAG, "Executing Runnable");
    }
});

mHandler.sendEmptyMessage(42);

The example posts two events to the message queue: first a Runnable followed by an empty message. As expected, with the sequential execution in mind, the Runnable is processed first, and consequently, is the first to be logged:

>>>>> Dispatching to Handler (android.os.Handler) {4111ef40}
      com.eat.MessageTracingActivity$1@41130820: 0
Executing Runnable
<<<<< Finished to Handler (android.os.Handler) {4111ef40}
      com.eat.MessageTracingActivity$1@41130820

The trace prints the start and end of the event identified by three properties:

Handler instance
android.os.Handler 4111ef40
Task instance
com.eat.MessageTracingActivity$1@41130820
The what parameter
0 (Runnable tasks do not carry a what parameter)

Similarly, the trace of an message with the what parameter set to 42 prints the message argument but not any Runnable instance:

>>>>> Dispatching to Handler (android.os.Handler) {4111ef40} null: 42
<<<<< Finished to Handler (android.os.Handler) {4111ef40} null

Combining the two techniques of message queue snapshots and dispatch tracing allows the application to observe message passing in detail.

Communicating with the UI Thread

The UI thread is the only thread in an application that has an associated Looper by default, which is associated on the thread before the first Android component is started. The UI thread can be a consumer, to which other threads can pass messages. It’s important to send only short-lived tasks to the UI thread. The UI thread is application global and processes both Android component and system messages sequentially. Hence, long-lived tasks will have a global impact across the application.

Messages are passed to the UI thread through its Looper that is accessible globally in the application from all threads with Looper.getMainLooper():

Runnable task = new Runnable() {...};
new Handler(Looper.getMainLooper()).post(task);

Independent of the posting thread, the message is inserted in the queue of the UI thread. If it is the UI thread that posts the message to itself, the message can be processed at the earliest after the current message is done:

// Method called on UI thread.
private void postFromUiThreadToUiThread() {
    new Handler().post(new Runnable() { ... });

    // The code at this point is part of a message being processed
    // and is executed before the posted message.

}

However, a task message that is posted from the UI thread to itself can bypass the message passing and execute immediately within the currently processed message on the UI thread with the convenience method Activity.runOnUiThread(Runnable):

// Method called on UI thread.
private void postFromUiThreadToUiThread() {
    runOnUiThread(new Runnable() { ... });

    // The code at this point is executed after the message.

}

If it is called outside the UI thread, the message is inserted in the queue. The runOnUiThread method can only be executed from an Activity instance, but the same behavior can be implemented by tracking the ID of the UI thread, for example, with a convenience method customRunOnUiThread in an Application subclass. The customRunOnUiThread inserts a message in the queue like the following example:

public class EatApplication extends Application {
    private long mUiThreadId;
    private Handler mUiHandler;

    @Override
    public void onCreate() {
        super.onCreate();
        mUiThreadId = Thread.currentThread().getId();
        mUiHandler = new Handler();
    }

    public void customRunOnUiThread(Runnable action) {
        if (Thread.currentThread().getId() != mUiThreadId) {
            mUiHandler.post(action);
        } else {
            action.run();
        }
    }
}

Summary

Android applications have access to the regular Java thread communication techniques, which suit worker-thread communication well. However, they rarely fit the use case when one of the threads is the UI thread, which is the most common case. Android message passing is used extensively throughout applications, either explicitly or implicitly, through various wrapping techniques that are discussed in the second part of this book.



[7] The UI thread is managed by the platform internal class android.app.ActivityThread.

Get Efficient Android Threading now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.