Source of the Problem:
The main thread prepares a message queue in the app program entryAccording to the Looper.loop() source code, it is a dead loop that traverses the message queue to fetch messages
Moreover, there is no related code that prepares a new thread for this dead loop to run, but the main thread does not freeze due to the dead loop in Looper.loop(). Why is that?For instance, methods like the lifecycle of Activity are executed in the main thread, right? How do these lifecycle methods execute outside of the dead loop?
Analysis:
To fully understand this issue, one needs to prepare knowledge in the following four areas: Process/Thread, Android Binder IPC, Handler/Looper/MessageQueue message mechanism, Linux pipe/epoll mechanism. To summarize, there are three main doubts: 1. Why does the main thread in Android not freeze due to the dead loop in Looper.loop()? 2. Why is there no related code that prepares a new thread for this dead loop to run? 3. The lifecycle methods of Activity are executed in the main thread, right? How do these lifecycle methods execute outside of the dead loop?—————————————————————————————–(1) Why does the main thread in Android not freeze due to the dead loop in Looper.loop()? This involves threads. First, let’s talk about processes/threads. Process: Each app creates a process when it runs, which is forked from Zygote to carry various components such as Activity/Service running on the app. The process is completely transparent to the upper application, which is also Google’s intention, allowing app programs to run on Android Runtime. Most of the time, an app runs in one process unless configured with the Android:process attribute in AndroidManifest.xml or forks a process through native code. Thread: Threads are very common for applications. For example, each time new Thread().start is called, a new thread is created. This thread shares resources with the app’s process. From the Linux perspective, there is no essential difference between processes and threads except for resource sharing; they are both task_struct structures. From the CPU’s perspective, processes or threads are just a piece of executable code, and the CPU uses the CFS scheduling algorithm to ensure each task enjoys CPU time slices as fairly as possible. With this preparation, let’s discuss the dead loop issue: since a thread is a piece of executable code, when the executable code is completed, the thread’s lifecycle should terminate, and the thread exits. However, for the main thread, we definitely do not want it to run for a while and then exit by itself. So how do we ensure it remains alive? A simple approach is to ensure the executable code can keep executing, and a dead loop can ensure it does not exit. For example, the binder thread also uses a dead loop to read and write operations with the Binder driver. Of course, it is not a simple dead loop; it sleeps when there are no messages. However, this may raise another question: since it is a dead loop, how does it handle other transactions? By creating a new thread. The operations that truly freeze the main thread occur during callback methods like onCreate/onStart/onResume, which may take too long, leading to dropped frames or even ANR. Looper.loop itself does not cause the application to freeze.(2) Why is there no related code that prepares a new thread for this dead loop to run? In fact, a new binder thread is created before entering the dead loop, in the code ActivityThread.main():
public static void main(String[] args) { .... // Create Looper and MessageQueue objects for handling main thread messages Looper.prepareMainLooper(); // Create ActivityThread object ActivityThread thread = new ActivityThread(); // Establish Binder channel (create new thread) thread.attach(false); Looper.loop(); // Message loop runs throw new RuntimeException("Main thread loop unexpectedly exited"); }
thread.attach(false); creates a Binder thread (specifically referring to ApplicationThread, the server of Binder, used to receive events sent by the system service AMS), and this Binder thread sends messages to the main thread through Handler. The specific process can be viewed in the startService process analysis, which will not be elaborated here. Briefly, Binder is used for inter-process communication and employs a C/S architecture. For friends interested in Binder, you can check another article:
Why does Android use Binder as the IPC mechanism? Additionally, ActivityThread is not actually a thread. Unlike the HandlerThread class, ActivityThread does not inherit the Thread class; it usually runs in the main thread, giving the impression of being a thread. The main thread carrying ActivityThread is created by forking from Zygote. Does the main thread’s dead loop consume a lot of CPU resources? In fact, it does not. This involves the Linux pipe/epoll mechanism. Simply put, when there are no messages in the main thread’s MessageQueue, it blocks in the loop’s queue.next() nativePollOnce() method, as detailed in the Android messaging mechanism 1-Handler (Java layer). At this point, the main thread releases CPU resources and enters a sleep state until the next message arrives or an event occurs, which wakes the main thread to work by writing data to the pipe. The epoll mechanism used here is an I/O multiplexing mechanism that can monitor multiple descriptors simultaneously. When a descriptor becomes ready (either for reading or writing), it immediately notifies the corresponding program to perform read or write operations, essentially synchronous I/O, meaning reading and writing are blocking. Therefore, the main thread is mostly in a sleeping state and does not consume a lot of CPU resources.(3) How do the Activity lifecycle methods execute outside of the dead loop? The internal class H of ActivityThread inherits from Handler, and the Handler message mechanism is simply used for inter-thread communication within the same process. The lifecycle of Activity relies on the main thread’s Looper.loop. When different Messages are received, corresponding measures are taken: In the H.handleMessage(msg) method, based on the received msg, corresponding lifecycle methods are executed. For example, when receiving msg=H.LAUNCH_ACTIVITY, the ActivityThread.handleLaunchActivity() method is called, which eventually creates an Activity instance through reflection and then executes Activity.onCreate() and other methods.
For example, when receiving msg=H.PAUSE_ACTIVITY, the ActivityThread.handlePauseActivity() method is called, which eventually executes Activity.onPause() and other methods. The above process only highlights the core logic; the actual process is much more complex. Where do the messages for the main thread come from? Of course, they are sent to the main thread by other threads in the app process through Handler. Please see the following content: Finally, from the perspective of communication between processes and threads, let’s enhance everyone’s understanding of the app running process with a diagram:
system_server process is a system process, the core carrier of the Java framework, running many system services, such as ApplicationThreadProxy (ATP) and ActivityManagerService (AMS). Both of these services run in different threads of the system_server process. Since ATP and AMS are both based on the IBinder interface, they are both binder threads, and the creation and destruction of binder threads are determined by the binder driver. The app process refers to the application program, where the main thread is mainly responsible for the lifecycle of Activity/Service components and UI-related operations. Additionally, each app process will have at least two binder threads: ApplicationThread (AT) and ActivityManagerProxy (AMP), in addition to the threads depicted in the diagram, there are many other threads such as the signal catcher thread, etc., which will not be listed one by one. Binder is used for communication between different processes, where a Binder client from one process sends transactions to the server of another process, while the handler is used for communication between different threads within the same process, as shown in the diagram where thread 2 sends transactions to thread 4.
Combining the diagram to discuss the Activity lifecycle, for example, when pausing an Activity, the process is as follows:
-
Thread 1’s AMS calls thread 2’s ATP; (Since threads in the same process share resources, they can call each other directly, but care must be taken with multi-thread concurrency issues)
-
Thread 2 transmits through binder to thread 4 in the App process;
-
Thread 4 sends a message to pause the Activity to the main thread through the handler message mechanism;
-
The main thread loops through messages in looper.loop(), when it receives the message to pause the Activity, it dispatches the message to ActivityThread.H.handleMessage() method, and after the method is called, it eventually calls Activity.onPause(). After handling onPause(), it continues looping in the loop.
Conclusion:
Who told you it doesn’t freeze? In fact, it has frozen. Don’t believe it? Just create a new thread, establish a looper inside it, then call looper.prepare, and then log something, and you will find that the log will never be printed. A looper maintains a message queue; if there are messages, it takes them out and executes them; if there are no messages, it sleeps until a message enters, which is a typical producer-consumer model. The actual message model is more complex, with timed and delayed execution related to system interrupts.
Therefore, after the looper starts, any code executed on the main thread is executed within a runnable taken from the message queue by a looper, meaning the main thread is effectively blocked in an infinite loop.
The activity lifecycle callback is also the same; the lifecycle callback is first sent by AMS through binder IPC to the app process. The binder stub in the app process receives the call and just inserts a runnable into the main looper (completed by the handler bound to the main looper).
Here is the source code of the main method in ActivityThread for version 6.0:
// End of event ActivityThreadMain.Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");
Looper.loop() is indeed a dead loop, but there is nothing after Looper.loop() (when the next statement executes, it means the program has exited abnormally).
Looper.loop() keeps running in the main thread, allowing our app to wait for user operations, so it does not execute the main() method and end.
Looper.loop() is called in this way:
// End of event ActivityThreadMain. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");
As mentioned in the question, Looper.loop() has an internal infinite loop, as long as the application is running, the loop of Looper.loop() will not exit. From the above code, if it exits from the loop of Looper.loop(), it will throw a Main thread loop unexpectedly exited exception. Android applications are event-driven; looper.loop() continuously receives and processes events. Every application runs under the control of Looper.loop(). If it stops, the application stops too. Only a specific application can block Looper.loop(), not Looper.loop() blocking the application.
About Java and AndroidExpert Channel
The Java and Android Expert Channel is a public account with tens of thousands of followers discussing Java and Android development, sharing and creating the most valuable articles.Let you become an expert in this field!
We discuss the cutting-edge technologies of Android and Java development: Android performance optimization, pluginization, cross-platform, dynamicization, strengthening and anti-cracking, etc., as well as design patterns/software architecture and more, by a team of engineers from BAT.
Follow us to receive red envelopes, reply: “Baidu”, “Alibaba”, “Tencent” for surprises!!! After following, you can join the WeChat group. The group consists of experts from Baidu, Alibaba, and Tencent.
We welcome you to follow us and discuss technology together. Scan and long-press the QR code below to quickly follow us. Or search for the public account: JANiubility.
Public account:JANiubility