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.


 Let us take the example of MediaPlayer, It initializes a Handler/Looper to handle different messages like.
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.









MediaPlayer creates a new EventHandler to handle the events and free the main thread.



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.



Let us see the implementation of prepare which essentially ensures that the Looper is a singleton.



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.









This function MediaPlayer::notify calls the MediaPlayerListener which was registered by the MediaPlayer. Let us see the registration process from a bottom up approach.





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);




 We now pass this Message instance to the handler for operating on it.
  






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].

Comments

  1. Excellent article. Very interesting to read. I really love to read such a nice article. Thanks! keep rocking. Text phone number

    ReplyDelete

Post a Comment

Popular posts from this blog

Android Audio Tutorial [Part Three] : AudioFlinger Introduction and Initialization

Android External Storage Support: Volume Daemon (vold) Architecture

Android Audio Tutorial [Part One] : Introduction