Tags

,

Lately, just for fun, I’ve been playing around with Android SDK. It is a really good developing environment, but there could be pitfalls if we are not careful.

In general, a good rule of thumb, in programming, is to catch errors as soon as possible. This means that catching errors at compile time is much better than catching them at runtime and it is easy to understand why

01140_android-cryingIn Android we have two nice objects related each others: Looper and Handler. Let’s see how they work and see how we can create a very dangerous situation, when the error is not detected at compile time and not even at runtime. But when we could see the App crashing, after a while, with an (not very nice) Out of Memory Error.

What’s a Handler in Android? “A Handler allows you to send and process Message and Runnable objects associated with a thread’s MessageQueue. Each Handler instance is associated with a single thread and that thread’s message queue”. But a Handler needs that the Thread has a message loop associated with it. It’s here that we need to define a Looper.

Android’s API define a Looper as a “Class used to run a message loop for a thread. Threads by default do not have a message loop associated with them; to create one, call prepare() in the thread that is to run the loop, and then loop() to have it process messages until the loop is stopped”.

So, to properly initialize a Thread as a Looper we have to write something like this:

  class LooperThread extends Thread {
      public Handler mHandler;
      public void run() {
          Looper.prepare(); // Associate the MessageQueue
          mHandler = new Handler() {
              public void handleMessage(Message msg) {
                  // process incoming messages here
              }
          };
          Looper.loop(); // Start to loop...
      }
  }

If we try to associate a Handler to a Thread that didn’t prepare a Looper we get back a:

java.lang.RuntimeException:
Can't create handler inside thread that has not called Looper.prepare()"

This is ok. Not perfect, because our code still compiles but at least, at Runtime, we immediately see that we did something wrong.

But now let’s be a bit more careless. What happens if we forget only the call Looper.loop()? So if we try to associate a Handler to a Thread that did prepare the Looper, but forgot to start looping?

It’s here that the dangerous situation starts. We don’t get any compile errors and, unfortunately, we don’t get any Runtime errors either. Our code not only compiles, but it does run.

We associated a Handler to a Thread with a MessageQueue, but this Thread is not looping and checking for messages!!

Now let’s analyze a particular situation. Let’s pretend we want to use the LocationManager to listen to some GPS NMEA Sentences from our smartphone. So, we need  to register a NmeaListener and we need to listen to onNmeaReceived() callbacks.

Registering a NmeaListener means:

// LocationManager.java - Android source code
public boolean addNmeaListener(GpsStatus.NmeaListener listener) {
    boolean result;
    // cut-code
    // (...)
    GpsStatusListenerTransport transport = new GpsStatusListenerTransport(listener);
    // cut-code
    // (...)
    return result;
}

We are creating a GpsStatusListenerTransport. This object keeps an internal buffer called mNmeaBuffer. In this buffer all the the NMEA objects are put at every onNmeaReceived() the OS callback to the LocationManager:

// LocationManager.java - Android source code
public void onNmeaReceived(long timestamp, String nmea) {
    if (mNmeaListener != null) {
        synchronized (mNmeaBuffer) {
            mNmeaBuffer.add(new Nmea(timestamp, nmea));
        }
        // cut-code
        // (...)
        mGpsHandler.sendMessage(msg);
     }
}

In this code you can see that GpsStatusListenerTransport has a Handler called mGpsHandler, and that it sends messages to this Handler every time a Nmea sentence is received.

This means that if we try to register a Nmea listener from a Thread that didn’t prepare a Looper, we will get the RuntimeException we saw before. But, what if we register a Nmea listener from a Thread that prepared the Looper but forgot to start it?

If we have a look at how mGpsHandler handles the messages sent from onNmeaReceived():

// LocationManager.java - Android source code
private final Handler mGpsHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        if (msg.what == NMEA_RECEIVED) {
            synchronized (mNmeaBuffer) {
                int length = mNmeaBuffer.size();
                for (int i = 0; i < length; i++) {
                    Nmea nmea = mNmeaBuffer.get(i);
                    mNmeaListener.onNmeaReceived(nmea.mTimestamp, nmea.mNmea);
                }
                mNmeaBuffer.clear();
        }
  // cut-code
  // (...)
};

We see that this Handler is very important. Not only because it is the one who should callback to our listener:

// No Looper.loop() = No callback!
mNmeaListener.onNmeaReceived(nmea.mTimestamp, nmea.mNmea);

But especially because the Handler is charged to clean the mNmeaBuffer:

// No Looper.loop() = No clearing the buffer!!!!
 mNmeaBuffer.clear();

And this means that mNmeaBuffer will grow at every onNmeaReceived() the OS will call on the GpsStatusListenerTransport, and it will never be emptied, because the Looper is not looping and so the Handler is not handling the messages!

Try to write a little app that has this bug and then try to walk around with the GPS turned on. According to your device and its max heap size, you’ll soon see your App crashing with: OutOfMemoryError 🙂

So, when using Looper in Android: be careful! 😀

Advertisements