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).
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
.
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
);
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.
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. Theread()
method blocks whenever there is no data to read from the pipe. It’s worth noticing that theread()
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, thePipedReader
uses a blocking call towait()
with one-second timeout. Hence, if theflush()
call is omitted, the consumer thread may delay the reading of data up to one second. By callingflush()
, the producer cuts short the wait in the consumer thread and allows data processing to continue immediately.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.
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.
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).
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).
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.
-
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
—andThread
—has at most oneMessageQueue
. -
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.
-
Insert: The producer thread inserts messages in the queue by using the
Handler
connected to the consumer thread, as shown in Handler. -
Retrieve: The
Looper
, discussed in Looper, runs in the consumer thread and retrieves messages from the queue in a sequential order. -
Dispatch: The handlers are responsible for processing the messages on the consumer thread. A thread may have multiple
Handler
instances for processing messages; theLooper
ensures that messages are dispatched to the correctHandler
.
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
{
public
Handler
mHandler
;
public
void
run
()
{
Looper
.
prepare
();
mHandler
=
new
Handler
()
{
public
void
handleMessage
(
Message
msg
)
{
if
(
msg
.
what
==
0
)
{
doLongRunningOperation
();
}
}
};
Looper
.
loop
();
}
}
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
mLooperThread
=
new
LooperThread
();
mLooperThread
.
start
();
}
public
void
onClick
(
View
v
)
{
if
(
mLooperThread
.
mHandler
!=
null
)
{
Message
msg
=
mLooperThread
.
mHandler
.
obtainMessage
(
0
);
mLooperThread
.
mHandler
.
sendMessage
(
msg
);
}
}
private
void
doLongRunningOperation
()
{
// Add long running operation here.
}
protected
void
onDestroy
()
{
mLooperThread
.
mHandler
.
getLooper
().
quit
();
}
}
Definition of the worker thread, acting as a consumer of the message queue.
Associate a
Looper
—and implicitly aMessageQueue
—with the thread.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 theLooper
of the current thread. Hence, thisHandler
can created only afterLooper.prepare()
, or it will have nothing to bind to.Callback that runs when the message has been dispatched to the worker thread. It checks the
what
parameter and then executes the long operation.Start dispatching messages from the message queue to the consumer thread. This is a blocking call, so the worker thread will not finish.
Start the worker thread, so that it is ready to process messages.
There is race condition between the setup of
mHandler
on a background thread and this usage on the UI thread. Hence, validate thatmHandler
is available.Initialize a
Message
-object with thewhat
argument arbitrarily set to 0.Insert the message in the queue.
Terminate the background thread. The call to
Looper.quit()
stops the dispatching of messages and releasesLooper.loop()
from blocking so therun
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).
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.
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
);
Looper
.
loop
();
}
@Override
public
boolean
queueIdle
()
{
if
(
mIsFirstIdle
)
{
mIsFirstIdle
=
false
;
return
true
;
}
mConsumerHandler
.
getLooper
().
quit
();
return
false
;
}
public
void
enqueueData
(
int
i
)
{
mConsumerHandler
.
sendEmptyMessage
(
i
);
}
}
Register the
IdleHandler
on the background thread when it is started and theLooper
is prepared so that theMessageQueue
is set up.Let the first
queueIdle
invocation pass, since it occurs before the first message is received.Return
true
on the first invocation so that theIdleHandler
still is registered.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 parametersParameter 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 thedata
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.
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
();
// Handler creation omitted.
Looper
.
loop
();
}
}
The first step is to create the
Looper
, which is done with the staticprepare()
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.Start handling messages in the message queue. This is a blocking method that ensures the
run()
method is not finished; whilerun()
blocks, theLooper
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()
throwsRuntimeException
. -
The runtime associates a
Looper
to the UI thread byLooper.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 theLooper
of the current thread:new
Handler
();
new
Handler
(
Handler
.
Callback
)
Constructors with an explicit
Looper
bind to thatLooper
: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.
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.
Failure | Error response | Typical application problem |
Message has no |
| Message was created from a |
Message has already been dispatched and is being processed. |
| The same message instance was inserted twice. |
Looper has exited. |
| Message is inserted after |
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
();
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
();
}
});
}
@Override
protected
void
onDestroy
()
{
super
.
onDestroy
();
mBackgroundThread
.
exit
();
}
// ... The rest of the Activity is defined further down
}
A background thread with a message queue is started when the
HandlerExampleActivity
is created. It handles tasks from the UI thread.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.
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
()
{
Looper
.
prepare
();
mBackgroundHandler
=
new
Handler
();
Looper
.
loop
();
}
public
void
doWork
()
{
mBackgroundHandler
.
post
(
new
Runnable
()
{
@Override
public
void
run
()
{
Message
uiMsg
=
mUiHandler
.
obtainMessage
(
SHOW_PROGRESS_BAR
,
0
,
0
,
null
);
mUiHandler
.
sendMessage
(
uiMsg
);
Random
r
=
new
Random
();
int
randomInt
=
r
.
nextInt
(
5000
);
SystemClock
.
sleep
(
randomInt
);
uiMsg
=
mUiHandler
.
obtainMessage
(
HIDE_PROGRESS_BAR
,
randomInt
,
0
,
null
);
mUiHandler
.
sendMessage
(
uiMsg
);
}
});
}
public
void
exit
()
{
mBackgroundHandler
.
getLooper
().
quit
();
}
}
Associate a
Looper
with the thread.The
Handler
processes onlyRunnables
. Hence, it is not required to implementHandler.handleMessage
.Post a long task to be executed in the background.
Create a
Message
object that contains only awhat
argument with a command—SHOW_PROGRESS_BAR
—to the UI thread so that it can show the progress bar.Send the start message to the UI thread.
Simulate a long task of random length, that produces some data
randomInt
.Create a
Message
object with the resultrandomInt
, that is passed in thearg1
parameter. Thewhat
parameter contains a command—HIDE_PROGRESS_BAR
—to remove the progress bar.The message with the end result that both informs the UI thread that the task is finished and delivers a result.
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:
mProgressBar
.
setVisibility
(
View
.
VISIBLE
);
break
;
case
HIDE_PROGRESS_BAR:
mText
.
setText
(
String
.
valueOf
(
msg
.
arg1
));
mProgressBar
.
setVisibility
(
View
.
INVISIBLE
);
break
;
}
}
};
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 therun
method of theRunnable
, which is executed automatically on the consumer thread, without invokingHandler.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 theHandler.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
{
@Override
public
boolean
handleMessage
(
Message
msg
)
{
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
}
};
handler
.
sendEmptyMessage
(
1
);
handler
.
sendEmptyMessage
(
2
);
}
}
The
HandlerCallbackActivity
implements theCallback
interface to intercept messages.The
Callback
implementation intercepts messages. Ifmsg.what
is 1, it returnstrue
—the message is handled. Otherwise, it changes the value ofmsg.what
to 22 and returnsfalse
—the message is not handled, so it is passed on to theHandler
implementation ofhandleMessage
.Process messages in the second
Handler
.Insert a message with
msg.what == 1
. The message is intercepted by theCallback
as it returnstrue
.Insert a message with
msg.what == 2
. The message is changed by theCallback
and passed on to theHandler
that printsSecondary 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.
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 |
| 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
();
Handler
handler
=
new
Handler
()
public
void
handleMessage
(
Message
msg
)
{
// Process message
Log
.
d
(
"Example"
,
"Processing message"
);
}
};
Message
message
=
handler
.
obtainMessage
(
0
,
tag
);
handler
.
sendMessage
(
message
);
handler
.
postAtTime
(
new
Runnable
()
{
public
void
run
()
{
// Left empty for brevity
}
},
tag
,
SystemClock
.
uptimeMillis
());
handler
.
removeCallbacksAndMessages
(
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.
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 awhat
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.
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.