For an Android developer, you should be familiar with Handler, Looper and Message. Here we briefly introduce the relationship between them, and then do something interesting with Looper.loop method to deepen the understanding of the running cycle.
Source Code Understanding Handler, Looper, Message
Usually when we use Handler, we will send a new Handler in the main thread to receive messages. Let's look at the Handler source code:
/**
* Default constructor associates this handler with the {@link Looper} for the
* current thread.
*
* If this thread does not have a looper, this handler won't be able to receive messages
* so an exception is thrown.
*/
public Handler() {
this(null, false);
}
In the source comment, it is said that the default constructor creates Handler, which takes Looper out of the current thread. If the current thread does not have Looper, the Handler cannot receive messages and throws exceptions.
Let's move on:
public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
//Get Looper
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static Looper myLooper() {
return sThreadLocal.get();//Get from ThreadLocal
}
Since Looper was acquired from ThreadLocal, there must be time to save it. Let's see when Looper was saved:
/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
That is to say, when we call the Looper. prepare method, we create Looper and store it in ThreadLocal. Note that the default quitAllowed parameter is true, that is, the default created Looper can exit. We can click in to see:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
//Enter MessageQueue.java
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
Note: The member variable mQuitAllowed of MessageQueue will enter MessageQueue to judge mQuitAllowed when calling Looper.quit method. You can simply look at the source code. Later we will talk about:
//MessageQueue.java
void quit(boolean safe) {
//If mQuitAllowed is false, an exception will be reported if exit is not allowed.
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr);
}
}
We should have doubts when we see this.
- The first question: By default when we call the Looper.prepare method, the mQuitAllowed variable is true, so when is it false and how is it set to false?
- The second question: When we created the Handler, we did not store Looper in ThreadLocal, but directly removed Looper from ThreadLocal. When did this Looper be created and saved?
Here's the main method in ActivityThread. When the Zygote process hatches a new application process, it executes the main method of the ActivityThread class. In this method, Looper and message queue are prepared, Looper is stored in ThreadLocal, then attach method is called to bind application process to Activity Manager Service, and then enter loop to read messages in message queue continuously and distribute messages.
//ActivityThread
public static void main(String[] args) {
SamplingProfilerIntegration.start();
// CloseGuard defaults to true and can be quite spammy. We
// disable it here, but selectively enable it later (via
// StrictMode) on debug builds, but using DropBox, not logs.
CloseGuard.setEnabled(false);
Environment.initForCurrentUser();
// Set the reporter for event logging in libcore
EventLogger.setReporter(new EventLoggingReporter());
Process.setArgV0("<pre-initialized>");
//Create a blocking queue for the main thread
Looper.prepareMainLooper();
// Create an ActivityThread instance
ActivityThread thread = new ActivityThread();
//Perform initialization
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
AsyncTask.init();
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
//Open cycle
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
Let's look at the open loop:
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
//Notice here that msg.target is the Andler sending MSG
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
}
The Looper.loop method is internally a dead loop (for(;;). queue.next(); Mesage that takes the head from the blocked queue and blocks the main thread when there is no Message. view rendering, event distribution, activity startup, activity life cycle callback and so on are all Message one by one. The system inserts these messages into the only queue in the main thread, and all messages are queued for the execution of the main thread.
Let's take a look back. First, we created Handler in the main thread. In the construction method of Handler, we will judge whether or not we created Looper. Because we initialized Looper in the ActivityThread.main method and stored it in ThreadLocal, we can create Handler normally. (If you don't create a handler in the main thread, you need to call the Looper.prepare method manually before creating it.) In Looper's construction method, a MessageQueue Message queue is created to access the Message. Then, Handler.sendMessage sends messages, stores messages in MessageQueue in queue. enqueueMessage (msg, uptime Millis) method, and finally calls msg.target.dispatchMessage(msg) in Loop.loop; that is, dispatchMessage method of Handler sending messages processes messages, and finally calls handleMessage(msg) method in dispatchMessage. In this way, we can process the messages sent to the main thread normally.
2. Do things with Looper
- Blocking threads for asynchronous tasks to allow programs to execute in the desired order
- Determine whether the main thread is blocked
- Prevent program from abnormal crash
1. Blocking threads for asynchronous tasks to allow programs to execute sequentially as needed
When dealing with asynchronous tasks, we usually pass in callbacks to handle the logic of successful or failed requests, and we can also use Looper to process messages sequentially without using callbacks. Let's take a look:
String a = "1";
public void click(View v){
new Thread(new Runnable() {
@Override
public void run() {
//Simulated time-consuming operation
SystemClock.sleep(2000);
a = "22";
mHandler.post(new Runnable() {
@Override
public void run() {
mHandler.getLooper().quit();
}
});
}
}).start();
try{
Looper.loop();
}catch (Exception e){
}
Toast.makeText(getApplicationContext(),a,Toast.LENGTH_LONG).show();
}
When we click the button, we start the thread processing time-consuming operation, and then call Looper.loop(); the method handles the Message loop, which means that the main thread starts to read and execute messages in queue continuously. So when mHandler.getLooper().quit(); the quit method of MessageQueue is called:
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
...
}
This is the variable mQuitAllowed, which we analyzed before. The main thread is not allowed to exit. Exceptions will be thrown here. Finally, this code is executed by calling msg.target.dispatchMessage to get the message in Looper.loop method. We capture the exception of Looper.loop, and then the code continues to execute and pops up Toast.
2. Determine whether the main thread is blocked
Generally speaking, the Loop.loop method constantly takes out the Message and calls its bound Handler to perform the main thread refresh operation in the UI thread.
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
//Notice here the msg.targetFor sending msg Of Handler
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
That's basically msg.target.dispatchMessage(msg); we can judge whether UI threads are time-consuming based on the execution time of this line of code.
In msg.target.dispatchMessage(msg); before and after, there are logging judgments and printing > > > > > Dispatching to and <<<< Finished to logs, we can set logging and print the corresponding time, basically we can judge the consumption time.
Looper.getMainLooper().setMessageLogging(new Printer() {
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";
@Override
public void println(String x) {
if (x.startsWith(START)) {
//start
}
if (x.startsWith(END)) {
//End
}
}
});
3. Prevent abnormal program crash
Since the main thread exception events eventually occur in the Looper.loop call, we capture the exception in the Looper.loop method, and then the main thread exception will not cause the program exception:
private Handler mHandler = new Handler();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_test);
mHandler.post(new Runnable() {
@Override
public void run() {
while (true){
try{
Looper.loop();
}catch (Exception e){
}
}
}
});
}
public void click2(View v){
int a = 1/0;//Error reporting when divisor is 0
}
All exceptions to the main thread are thrown from the Loper. loop we call manually, and once thrown, they are caught by try{}catch, so that the main thread does not crash. Open source projects for this principle: Cockroach If you are interested, you can see the implementation.