When facing bugs that were related to how we interact with the main thread, I decided to get a closer look at what the main thread really is.
1 2 3 4 5 |
|
All Java programs start with a call to a public static void main()
method. This is true for Java Desktop programs, JEE servlet containers, and Android applications.
When the Android system boots, it starts a Linux process called ZygoteInit
. This process is a Dalvik VM that loads the most common classes of the Android SDK on a thread, and then waits.
When starting a new Android application, the Android system forks the ZygoteInit
process. The thread in the child fork stops waiting, and calls ActivityThread.main()
.
Loopers
Before going any further, we need to look at the Looper
class.
Using a looper is a good way to dedicate one thread to process messages serially.
Each looper has a queue of Message
objects (a MessageQueue
).
A looper has a loop()
method that will process each message in the queue, and block when the queue is empty.
The Looper.loop()
method code is similar to this:
1 2 3 4 5 6 7 |
|
Each looper is associated with one thread. To create a new looper and associate it to the current thread, you must call Looper.prepare()
. The loopers are stored in a static ThreadLocal
in the Looper
class. You can retrieve the Looper
associated to the current thread by calling Looper.myLooper()
.
The HandlerThread
class does everything for you:
1 2 3 |
|
Its code is similar to this:
1 2 3 4 5 6 7 8 |
|
Handlers
A handler is the natural companion to a looper.
A handler has two purposes:
- Send messages to a looper message queue from any thread.
- Hndle messages dequeued by a looper on the thread associated to that looper.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
You can associate multiple handlers to one looper. The looper delivers the message to message.target
.
A popular and simpler way to use a handler is to post a Runnable
:
1 2 3 4 5 6 |
|
A handler can also be created without providing any looper:
1 2 |
|
The handler no argument constructor calls Looper.myLooper()
and retrieves the looper associated with the current thread. This may or may not be the thread you actually want the handler to be associated with.
Most of the time, you just want to create a handler to post on the main thread:
1
|
|
Back to PSVM
Let’s look at ActivityThread.main()
again. Here is what it is essentially doing:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Now you know why this thread is called the main thread :) .
Note: As you would expect, one of the first things that the main thread will do is create the Application
and call Application.onCreate()
.
Activities love orientation changes
Let’s start with the activity lifecycle and the magic behind the handling of configuration changes.
Why it matters
This article was inspired by a real crash that occurred in Square Register. A simplified version of the code is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
As we will see, doSomething()
can be called after the activity onDestroy()
method has been called due to a configuration change. At that point, you should not use the activity instance anymore.
A refresher on orientation changes
The device orientation can change at any time. We will simulate an orientation change while the activity is being created using Activity#setRequestedOrientation(int)
.
Can you predict the log output when starting this activity in portrait?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
If you know the Android lifecycle, you probably predicted this:
1 2 3 4 5 6 7 |
|
The Android Lifecycle goes on normally, the activity is created, resumed, and then the orientation change is taken into account and the activity is paused, destroyed, and a new activity is created and resumed.
Orientation changes and the main thread
Here is an important detail to remember: an orientation change leads to recreating the activity via a simple post of a message to the main thread looper queue.
Let’s look at that by writing a spy that will read the content of the looper queue via reflection:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
As you can see, the message queue is merely a linked list where each message has a reference to the next message.
We log the content of the queue right after the orientation change:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Here is the output:
1 2 3 4 5 6 |
|
A quick look at the ActivityThread
class tells us what those 118 and 126 messages are:
1 2 3 4 5 6 |
|
Requesting an orientation change added CONFIGURATION_CHANGED
and a RELAUNCH_ACTIVITY
message to the main thread looper queue.
Let’s take a step back and think about what’s going on:
When the activity starts for the first time, the queue is empty. The message currently being executed is LAUNCH_ACTIVITY
, which creates the activity instance, calls onCreate()
and then onResume()
in a row. Then only the main looper processes the next message in the queue.
When a device orientation change is detected, a RELAUNCH_ACTIVITY
is posted to the queue.
When that message is processed, it:
- calls
onSaveInstanceState()
,onPause()
,onDestroy()
on the old activity instance, - creates a new activity instance,
- calls
onCreate()
andonResume()
on that new activity instance.
All that in one message handling. Any message you post in the meantime will be handled after onResume()
has been called.
Tying it all together
What could happen if you post to a handler in onCreate()
during an orientation change? Let’s look at the two cases, right before and right after the orientation change:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Here is the output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
To sum things up: at the end on onCreate()
, the queue contained four messages. The first was the post before the orientation change, then the two messages related to the orientation change, and then only the post after the orientation change. The logs show that these were executed in order.
Therefore, any message posted before the orientation change will be handled before onPause()
of the leaving activity, and any message posted after the orientation change will be handled after onResume()
of the incoming activity.
The practical implication is that when you post a message, you have no guarantee that the activity instance that existed at the time it was sent will still be running when the message is handled (even if you post from onCreate()
or onResume()
). If your message holds a reference to a view or an activity, the activity won’t be garbage collected until the message is handled.
What could you do?
The real fix
Stop calling handler.post()
when you are already on the main thread. In most cases, handler.post()
is used as a quick fix to ordering problems. Fix your architecture instead of messing it up with random handler.post()
calls.
If you have a good reason to post
Make sure your message does not hold a reference to an activity, as you would do for a background operation.
If you really need that activity reference
Remove the message from the queue with handler.removeCallbacks()
in the activity onPause()
.
If you want to get fired
Use handler.postAtFrontOfQueue()
to make sure a message posted before onPause()
is always handled before onPause()
. Your code will become really hard to read and understand. Seriously, don’t.
A word on runOnUiThread()
Did you notice that we created a handler and used handler.post()
instead of directly calling Activity.runOnUiThread()
?
Here is why:
1 2 3 4 5 6 7 8 9 |
|
Unlike handler.post()
, runOnUiThread()
does not post the runnable if the current thread is already the main thread. Instead, it calls run()
synchronously.
Services
There is a common misconception that needs to die: a service does not run on a background thread.
All service lifecycle methods (onCreate()
, onStartCommand()
, etc) run on the main thread (the very same thread that’s used to play funky animations in your activities).
Whether you are in a service or an activity, long tasks must be executed in a dedicated background thread. This background thread can live as long as the process of your app lives, even when your activities are long gone.
However, at any time the Android system can decide to kill the app process. A service is a way to ask the system to let us live if possible and be polite by letting the service know before killing the process.
Side note: When an IBinder
returned from onBind()
receives a call from another process, the method will be executed in a background thread.
Take the time to read the Service documentation — it’s pretty good.
IntentService
IntentService provides a simple way to serially process a queue of intents on a background thread.
1 2 3 4 5 6 7 8 9 |
|
Internally, it uses a Looper
to handle the intents on a dedicated HandlerThread
. When the service is destroyed, the looper lets you finish handling the current intent, and then the background thread terminates.
Conclusion
Most Android lifecycle methods are called on the main thread. Think of these callbacks as simple messages sent to a looper queue.
This article wouldn’t be complete without the reminder that goes into almost every Android dev article: Do not block the main thread.
REF:A journey on the Android Main Thread,A journey on the Android Main Thread – Lifecycle bits