Android Asynchronous Messaging Architecture : Android Looper Analysis.
Introduction to concurrency.
The
Java programming language and the Java virtual machine (JVM) have been designed
to support concurrent programming, and all execution takes place in the context
of threads. Objects and resources can be accessed by many separate threads;
each thread has its own path of execution but can potentially access any object
in the program. The programmer must ensure read and write access to objects is
properly coordinated (or "synchronized") between threads. Thread
synchronization ensures that objects are modified by only one thread at a time
and that threads are prevented from accessing partially updated objects during
modification by another thread. The Java language has built-in constructs to
support this coordination.
More
details on concurrency in Java [Remember Android apps are written primarily in
java programming language] can be obtained from.
Looper in Android.
In
Android, We more often than not have a situation wherein after a certain action
done on UI there happens to be a more intensive task [Network data read, file
read, codec operations etc].These operations should ideally never block the
main UI thread else the experience gets throttled and is very poor.
In
order to ensure that these operations do not affect the UI thread, We create a
new thread to execute our time-consuming code.
When
our time-consuming operation is completed, we need to update the UI interface or
the user that the operation is complete. This is where the concept of Loopers
and Handlers come into action.
Android
provides a message-driven mechanism for communication between multiple threads
which is based on the Java threading model and its core component is the Looper.
Android
Looper implementation includes three core components:
Message:
Represents an executable task
MessageQueue:
Queue, which receives the above messages through the handler for sequential
handling.
Handler:
Responsible for passing the messages to the message queue.
Looper:
Handles the messages one by one and calls handler.handleMessage() to process
them.
The
relationship between above components and their inter-working can be
illustrated simply by the following diagram.
Let
us spend sometime on analyzing each of these components from their inception.
Looper
initialization.
Before
we go ahead and see the Looper initialization, Let us pause and take an example
where this is used, It will give us more clear understanding of things around.
MEDIA_PREPARED
MEDIA_PLAYBACK_COMPLETE
MEDIA_STOPPED
MEDIA_SEEK_COMPLETE
and most importantly
MEDIA_ERROR
: To help MediaPlayer display an Error Message during a failure.
Let
us see this in the code.
We
already saw while creating the handler a looper instance is created.
Let us look at the file frameworks/base/core/java/android/os/Looper.java to see Looper’s components.
Let
us see some of the members and their importance.
1. sThreadLocal, is a variable of type
ThreadLocal<Looper>.
a. It consists of two
methods, set() and get().The type of the variable that requires thread
assignment is Looper.
b. sThreadLocal .set() sets
the current thread's Looper and copies to the specified value during sThreadLocal.get().ThreadLocal
can be thought to be a thread-level singleton.
2. static sMainLooper,
stored is the main thread (ie Looper of the UI thread).
a. It is static so that the
Looper can be obtained by
3. Handler.sendMessage()
adds Message to this queue for Looper.loop().
a. We will see how this
gets manifested to the native layer NativeMessageQueue which will lead to the
creation of the Native layer Looper.
4. The reference of the
thread where the Looper is located.
Simply
put the entire looper class could be viewed as below.
The
creation of the Looper thread singleton will cause the creation of the
MessageQueue. MessageQueue has a variable sMessages of type Message.
Let
us see the MessageQueue class.
The
MessageQueue constructor calls nativeInit to create the MessageQueue and be
used in the native [C++] layers, The pointer mPtr which is retrieved from the native layer is the mapped native layer NativeMessageQueue and hence incorporates the lower layer Looper implementations.
Let us see how.
Let
us see android_os_MessageQueue_nativeInit which is implemented in frameworks/base/core/jni/android_os_MessageQueue.cpp
We
are slowly entering the Native Looper implementation zone, Let us continue.
Let
us see the 2 important statements in the above code.
mLooper
= new Looper(false); // Create a new Looper object in the Native layer
Looper::setForThread(mLooper);
// Thread level singleton
Did
you notice an interesting thing?
The
creation Looper in the Java layer led to the creation of the MessageQueue and
it was just the opposite where the creation of NativeMessageQueue caused the
creation of Looper in the Native layer.
The
native layer code uses eventfd and linux epoll mechanism to monitor the writes
on the mWakeEventFd and wakeup. You can read more on eventfd and its use here.
This
mWakeEventFd is the key handling Java Messages and Native Messages using
Looper.
Let
us see the next important statement in native Looper creation rebuildEpollLocked.
The code is in system/core/libutils/Looper.cpp.
Creating
and Sending a message.
By
now we saw how Looper gets created and how its starts polling on the eventfd,
Let us move further and see exactly how a message is sent , gets passed and is handled.
Continuing
with our example of MediaPlayer, MediaPlayer.cpp has a function called notify
which essentially notifies the MediaPlayer UI to display error messages or
perform certain operations when a state is reached.
Let
us see this flow.
From
the file frameworks/av/media/libmedia/mediaplayer.cpp.
The
JNIMediaPlayerListener::notify calls the MediaPlayer java interface to notify
of error or events using the fields.post_event member which is regitered during native setup, Let us continue.
MediaPlayer.java sets this up while creating the MediaPlayer native instance and registers the handler.
When
postEventFromNative is called in MediaPlayer.java , It sends this Message to
the Handler using the MessageQueue to handle.
So
we saw how the native layer calls some event handling , This would ideally be
the same if a Java layer item wants a message handling outside of the main
thread.
Let
us get into the details of this Message passing and handling sequence.
From
the MediaPlayer::notify example we saw that on getting the postEventFromNative
Message
m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj);
mp.mEventHandler.sendMessage(m);
This
Message gets added to the MessageQueue.
This
wakes up the native Looper in the following way.
On
waking up the Looper writes this message to the evendfd so that the other part
which was polling gets this message to act on.
Polling of Data on eventfd.
Let us check the Looper::loop() function.
Some
important points to remember from the above loop code.
for
(;;) { // Run infinitely till someone calls Looper.quit
Message
msg = queue.next(); // Get the next java message
if
(msg == null) {
// No message indicates that
the message queue is quitting.
return;
}
msg.target.dispatchMessage(msg);
// This is the true
Message handling in java layer.
Note
that Message msg = queue.next() will block till the poll on the looper gets some
data /event to be processed, Let us see that.
In the above code, nativePollOnce(ptr, nextPollTimeoutMillis); blocks till the Looper in the native layer finds a new message. It then adds this message in the MessageQueue in the Java layer.
Let us check nativePollOnce(ptr, nextPollTimeoutMillis);
We
just saw above how the Looper loops and then once gets an event on the eventfd
it then reads this data and constructs this message and sends it to the upper
Layers.
Let
us comeback to the Looper::loop() function to see how the messages get handled using msg.target.dispatchMessage(msg);
Let
us see the message handling part.
Remember
while creating the Handler we sent the this pointer [Instance of the
MediaPlayer class] and that acts as the Callback class.
So
now, The handleMessage of the registering entity will be called to handle the
required message.
This completes our loop [remember we started with handleMessage in MediaPlayer.java which handles different events and messages].
We
have now covered the use of Looper and Handler to ensure asynchronous heavy tasks
are not done on the main/UI thread.
Remember Looper usage is more or less similar in the Native layers of Android [Example: The codec data processing which uses Looper].
Excellent article. Very interesting to read. I really love to read such a nice article. Thanks! keep rocking. Text phone number
ReplyDelete