[[194846]] [Quoted from CoorChice's blog] Background In the process of Android development, we almost cannot do without threads. But how much do you know about threads? How many unknown secrets are hidden behind its operation? How do threads communicate with each other and pass information? How do Looper, Handler, and MessageQueue operate behind the scenes? In this issue, let's start with Thread and gradually explore the secrets behind this powerful thread chain. Note that most of the analysis is in the code, so pay close attention to it! Start with the Thread creation process In this section, we will analyze the Thread creation process step by step. Without further ado, let’s look at the code directly. The starting point of thread creation is init() - // The public constructor that creates Thread calls this private init() method. Let's see what it does.
- /**
- *
- * @param thread group
- * @param is the Runnable classmate we usually come into contact with most
- * @param specifies the name of the thread
- * @param specifies the size of the thread stack
- */
- private void init(ThreadGroup g, Runnable target, String name , long stackSize) {
- Thread parent = currentThread();
- //First get the currently running thread. This is a native function, don't worry about how it does it for now. Black box thinking, haha!
- if (g == null ) {
- g = parent.getThreadGroup();
- //If ThreadGroup is not specified, the parent thread's TreadGroup will be obtained
- }
-
- g.addUnstarted();
- //Increase the ready thread counter in ThreadGroup by one. Note that the thread has not yet been actually added to the ThreadGroup.
- this.group = g;
- //Assign the group of the Thread instance . From here on, the thread owns the ThreadGroup.
-
- this.target = target;
- //Set Runnable to the Thread instance. It will be executed when start() is called later.
- this.priority = parent.getPriority();
- //Set the thread's priority weight to the parent thread's weight
- this.daemon = parent.isDaemon();
- //Determine whether the Thread instance is a daemon thread based on whether the parent thread is a daemon thread.
- setName( name );
- //Set the thread name
-
- init2(parent);
- // What? Another initialization, the parameter is still the parent thread. Don't worry, I'll look at it later.
-
- /* Stash the specified stack size in case the VM cares */
- this.stackSize = stackSize;
- //Set the thread stack size
- tid = nextThreadID();
- //Thread id. This is a static variable. Calling this method will increment it and use it as the thread id.
- }
The second init2() - private void init2(Thread parent) {
- this.contextClassLoader = parent.getContextClassLoader();
- //Set ClassLoader member variables
- this.inheritedAccessControlContext = AccessController.getContext();
- //Set access control environment
- if (parent.inheritableThreadLocals != null ) {
- this.inheritableThreadLocals = ThreadLocal.createInheritedMap(
- //Create a ThreadLocaleMap for the Thread instance. The ThreadLocaleMap of the parent thread is needed to copy a copy of the variables in the parent thread to the current thread.
- //ThreadLocaleMap is an array of Entry type, in which the Thread instance saves copies of variables.
- parent.inheritableThreadLocals);
- }
- }
At this point, our Thread has been initialized and several important member variables of Thread have been assigned values. Start the thread and drive! Usually, we start a thread like this. - Thread threadDemo = new Thread(() -> {
-
- });
- threadDemo.start();
So what kind of secret is hidden behind start()? Is it the distortion of human nature? Or the decline of morality? Let's click on start() and explore the secret behind start(). - //As we can see, this method is locked.
- //The reason is to prevent developers from calling this method of the same Thread instance in other threads, thereby avoiding exceptions as much as possible.
- //The reason why this method can execute the run() method in the Runnable we passed in is,
- //The JVM calls the run() method of the Thread instance.
- public synchronized void start() {
- // Check if the thread status is 0. If it is 0, it means it is a new state, that is, it has not been started (). If it is not 0, an exception is thrown.
- //That is to say, for a Thread instance, we can only call the start() method once.
- if (threadStatus != 0)
- throw new IllegalThreadStateException();
-
- //From here on, the real thread is added to the ThreadGroup group.
- // Let me repeat again, we just incremented the nUnstartedThreads counter, but did not add any threads.
- //At the same time, when the thread is started, the nUnstartedThreads counter will be -1. Because there is one less thread in the ready state!
- group . add ( this );
-
- started = false ;
- try {
- nativeCreate(this, stackSize, daemon);
- //This is another Native method. This is handled by the JVM, which calls the run() method of the Thread instance.
- started = true ;
- finally
- try {
- if (!started) {
- group .threadStartFailed(this);
- //If it is not started successfully, the Thread will be removed from the ThreadGroup.
- //At the same time, the nUnstartedThreads counter is incremented by 1 again.
- }
- } catch (Throwable ignore ) {
-
- }
- }
- }
Well, the most essential function is native, so let's treat it as a black box. Just know that it can call the run() method of the Thread instance. Then let's see what magical things the run() method does? - //Yes, it is that simple! Just call the run() method of the member variable target of the Runnable type.
- //At this point, the code we need to execute is executed.
- //As for the existence of this @Overrid, it is entirely because Thread itself is also a Runnable!
- //That is to say, our Thread can also be used as a Runnable.
- @Override
- public void run() {
- if (target != null ) {
- target.run();
- }
- }
Black Experiment - public void test_1() {
- Thread thread1 = new Thread(() -> {
- System. out .println(Thread.currentThread().getName());
- }, "Thread_1" );
-
-
- Thread thread2 = new Thread(thread1, "Thread_2" );
- thread2.start();
- }
-
-
-
- Output:
- Thread_2
The above experiment shows that we can use Thread as Runnable. Several common thread methods (operations) The untold secret of Thread.sleep() We usually use Thread.sleep() frequently, so let's study what happens when Thread.sleep() is called. Before we begin, let me introduce a concept - nanoseconds. 1 nanosecond = one billionth of a second. It can be seen that using it to time will be very accurate. However, due to equipment limitations, this value is sometimes not so accurate, but it is still much smaller than the control granularity of milliseconds. - //The Thread.sleep(long) method we usually call is called this method, and the last unfamiliar parameter is nanoseconds.
- // You can control threads at nanosecond level.
- public static void sleep(long millis, int nanos)
- throws InterruptedException {
- //The following three tests are to see if the millisecond and nanosecond settings are legal.
- if (millis < 0) {
- throw new IllegalArgumentException( "millis < 0: " + millis);
- }
- if (nanos < 0) {
- throw new IllegalArgumentException( "nanos < 0: " + nanos);
- }
- if (nanos > 999999) {
- throw new IllegalArgumentException( "nanos > 999999: " + nanos);
- }
-
-
- if (millis == 0 && nanos == 0) {
- if (Thread.interrupted()) {
- //When the sleep time is 0, check whether the thread is interrupted.
- // And clear the thread's interrupt status flag. This is a Native method.
- throw new InterruptedException();
- //If the thread's interrupt status is set to true (call Thread.interrupt()).
- //Then it will throw an exception. If you return the thread after catching the exception, the thread will stop.
- // Note that after calling Thread.sleep(), the result of calling isInterrupted() is always False .
- //Don't forget that Thread.interrupted() will clear the mark position while detecting!
- }
- return ;
- }
-
- long start = System.nanoTime();
- //Similar to System.currentTimeMillis(). But it gets nanoseconds, which may not be accurate.
- long duration = (millis * NANOS_PER_MILLI) + nanos;
-
- Object lock = currentThread().lock;
- //Get the lock of the current thread.
-
- synchronized (lock) {
- //Synchronize the lock object of the current thread
- while ( true ) {
- sleep(lock, millis, nanos);
- //This is another Native method, and it will also throw an InterruptedException.
- // According to my estimation, the duration of sleep after calling this function is uncertain.
-
- long now = System.nanoTime();
- long elapsed = now - start;
- //Calculate how long the thread has been sleeping
-
- if (elapsed >= duration) {
- //If the current sleep duration has met our needs, we will exit the loop and the sleep state will end.
- break;
- }
-
- duration -= elapsed;
- //Subtract the time you have slept and recalculate the time you need to sleep.
- start = now;
- millis = duration / NANOS_PER_MILLI;
- // Recalculate the milliseconds part
- nanos = ( int ) (duration % NANOS_PER_MILLI);
- // Recalculate the microseconds part
- }
- }
- }
From the above analysis, we can know that the core method of making a thread sleep is a Native function sleep(lock, millis, nanos), and its sleep duration is uncertain. Therefore, the Thread.sleep() method uses a loop to check whether the sleep duration meets the requirement each time. At the same time, it should be noted that if the thread's interrupted state is set to true when calling the sleep() method, an InterruptedException will be thrown before starting the sleep loop. What is Thread.yield() hiding? This method is native. Calling this method can prompt the CPU that the current thread will give up the current CPU usage rights and compete with other threads for new CPU usage rights. The current thread may or may not be able to execute again. That's it. What is the ubiquitous wait()? You must have often seen that no matter which object instance, there will be several methods named wait() at the bottom. Wait? What kind of existence are they? Let's click to see. Oh my god, they are all Native functions. [[194847]] Then take a look at the documentation to see what it is. According to the description in the document, wait(), together with notify() and notifyAll(), can realize inter-thread communication, i.e. synchronization. When calling wait() in a thread, it must be called in a synchronized code block, otherwise an IllegalMonitorStateException will be thrown. This is because the wait() function needs to release the lock of the corresponding object. When the thread executes wait(), the object will put the current thread into its own thread pool, release the lock, and then block it. Until the object calls notify() or notifyAll(), the thread can regain, or possibly obtain, the lock of the object and continue to execute the following statements. Uh... OK, let me explain the difference between notify() and notifyAll(). After calling notify(), the object will randomly select a thread from its own thread pool (that is, the thread that called the wait() function on the object) to wake it up. That is, only one thread can be woken up at a time. If you only call notify() once in a multi-threaded environment, only one thread can be woken up, and the other threads will always be in the same thread pool. After calling notifyAll(), the object will wake up all threads in its thread pool, and then these threads will grab the lock of the object together. Digging the love-hate relationship between Looper, Handler, and MessageQueue We may have written code like this in the past: - new Thread(()->{
-
- ...
- Looper.prepare ();
- Handler handler = new Handler(){
- @Override
- public void handleMessage(Message msg) {
- super.handleMessage(msg);
- }
- };
- Looper.loop();
-
- }).start()
Many students know that when using Handler in a thread (except the Android main thread), it must be placed between Looper.prepare() and Looper.loop(). Otherwise, a RuntimeException will be thrown. But why do we do this? Let's take a look at the inside story. [[194848]] Start with Looper.prepare() What happens when Looper.prepare() is called? - public static void prepare () {
- prepare ( true );
- //Finally, the logic in the private method prepare (boolean quitAllowed) is executed
- }
-
- private static void prepare (boolean quitAllowed) {
- if (sThreadLocal.get() != null ) {
- //First try to check whether there is already a Looper in the current thread, if so, throw an exception.
- //This is why we can't call Looper.prepare () twice in a Thread.
- throw new RuntimeException( "Only one Looper may be created per thread" );
- }
- sThreadLocal.set ( new Looper(quitAllowed));
- //***If called, create a new Looper.
- }
-
- //Looper's private constructor
- private Looper(boolean quitAllowed) {
- mQueue = new MessageQueue(quitAllowed);
- //Create a new MessageQueue, I will come back to it later.
- mThread = Thread.currentThread();
- //Assign the current thread to mThread.
- }
After the above analysis, we already know what happens after Looper.prepare() is called. But here comes the problem! sThreadLocal is a static ThreadLocal<Looper> instance (in Android, the type of ThreadLocal is fixed to Looper). That is, all threads in the current process share this ThreadLocal<Looper>. So, since Looper.prepare() is a static method, how does Looper determine which thread it should establish a binding relationship with now? Let's dig deeper. Let's take a look at ThreadLocal's get() and set() methods. - public T get() {
- Thread t = Thread.currentThread();
- //The key point! Get the currently running thread.
- ThreadLocalMap map = getMap(t);
- //Get the ThreadLocalMap of the current thread. This is a key point, which has been mentioned before.
- //Students who have forgotten can look at it again in front.
- if (map != null ) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- //As you can see, there is a <ThreadLocal, Looper> key-value pair in the ThreadLocalMap of each thread.
- //The binding relationship is established through this key-value pair.
- if (e != null )
- return (T)e.value;
- }
- return setInitialValue();
- }
-
- public void set (T value) {
- Thread t = Thread.currentThread();
- //Also get the current thread first
- ThreadLocalMap map = getMap(t);
- //Get the thread's ThreadLocalMap
- if (map != null )
- map.set (this, value);
- //Store key-value pairs
- else
- createMap(t, value);
- }
Creating a Handler Handler can be used to achieve communication between threads. In Android, when we finish data processing in the child thread, we often need to use Handler to notify the main thread to update the UI. Usually we use new Handler() to create a Handler instance in a thread, but how does it know which thread's task it should handle? Let's take a look at Handler together. - public Handler() {
- this( null , false );
- }
-
- public Handler(Callback callback, boolean async) { //As you can see, this method is finally called.
- 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());
- }
- }
-
- mLooper = Looper.myLooper();
- //The key point! Here the Handler is bound to the Looper of the current Thread.
- //Looper.myLooper() takes out the Looper of the current thread from ThreadLocale.
- if (mLooper == null ) {
- //If Looper.prepare () is not called before new Handler() in the child thread, the Looper of the current thread has not been created yet.
- //This exception will be thrown.
- throw new RuntimeException(
- "Can't create handler inside thread that has not called Looper.prepare()" );
- }
- mQueue = mLooper.mQueue;
- //Assign Looper's MessageQueue to Handler.
- mCallback = callback;
- mAsynchronous = async;
- }
Looper.loop() We all know that after the Handler is created, Looper.loop() needs to be called, otherwise sending messages to the Handler is useless! Next, let's take a look at what kind of magic Looper has that can accurately send messages to the Handler for processing. - public static void loop() {
- final Looper me = myLooper();
- //This method has been mentioned before, which is to get the Looper object in the current thread.
- if (me == null ) {
- //If there is no Looper.prepare (), an error will be reported!
- throw new RuntimeException( "No Looper; Looper.prepare() wasn't called on this thread." );
- }
- final MessageQueue queue = me.mQueue;
- //Get the MessageQueue member variable of Looper, which is new when Looper is created.
-
- //This is a native method, which is used to detect whether the current thread belongs to the current process. And it will continue to track its true identity.
- //In the IPC mechanism, this method is used to clear the pid and uid information of IPCThreadState. And return an identity to facilitate restoration using restoreCallingIdentity().
- Binder.clearCallingIdentity();
- final long ident = Binder.clearCallingIdentity();
-
- for (;;) {
- //Key point (knock on the blackboard)! This is an infinite loop, waiting to extract messages and send messages.
- Message msg = queue. next ();
- // Extract a message from the MessageQueue. We will see how to extract it later.
- 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
- final Printer logging = me.mLogging;
- if (logging != null ) {
- logging.println( ">>>>> Dispatching to " + msg.target + " " +
- msg.callback + ": " + msg.what);
- }
-
- final long traceTag = me.mTraceTag; //Get the trace tag of MessageQueue
- if (traceTag != 0) {
- Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
- //Start tracking the current message in the MessageQueue of this thread. This is a Native method.
- }
- try {
- msg.target.dispatchMessage(msg);
- //Try to dispatch the message to the Handler bound to the Message
- finally
- if (traceTag != 0) {
- Trace.traceEnd(traceTag);
- //This is used in conjunction with Trace.traceBegin().
- }
- }
-
- if (logging != null ) {
- logging.println( "<<<<< Finished to " + msg.target + " " + msg.callback);
- }
-
-
- final long newIdent = Binder.clearCallingIdentity();
- //what? This Native method is called again. This is mainly to verify again whether the process where the thread is located has changed.
- 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();
- //Recycle the release message.
- }
- }
From the above analysis, we can know that after calling Looper.loop(), the thread will be blocked by a for(;;) infinite loop, waiting for the next() method of MessageQueue to take out a Message before continuing to execute. Then the corresponding Handler (target member variable) is obtained through the Message, and the Handler dispatches the Message to handleMessage() for processing through the dispatchMessage() method. It should be noted here that when the thread is in a loop, the thread will remain in the loop. This means that the code after Looper.loop() cannot be executed. If you want to execute it, you need to exit the loop first. - Looper myLooper = Looper.myLoop();
- myLooper.quit(); //Normal exit method.
- myLooper.quitSafely(); //Safe way to exit.
Now another question arises, how does the next() method of MessageQueue block the thread? Next, let's take a look at the MessageQueue behind the scenes. MessageQueue behind the scenes MessageQueue is a single-link data structure that maintains a message list. - Message next () {
- // Check if the loop has exited. mPrt is the address of the MessageQueue of the Native layer.
- //This address can be used to interact with the Native layer's MessageQueue.
- final long ptr = mPtr;
- if (ptr == 0) {
- return null ;
- }
-
- int pendingIdleHandlerCount = -1;
- int nextPollTimeoutMillis = 0;
- //Time stamp, it is 0 only when the message is obtained for the first time. Because it is outside the infinite loop!
- for (;;) {
- if (nextPollTimeoutMillis != 0) {
- Binder.flushPendingCommands();
- //If it is not the first time to get the message, call the Native function to let the virtual machine refresh all the hungry Binder commands,
- // Ensure that the process releases previous objects before performing tasks that may block.
- }
-
- //This is a Native method.
- nativePollOnce(ptr, nextPollTimeoutMillis);
-
- synchronized (this) { //Lock MessageQueue
- //Get the current system time for comparison with msg.when later .
- final long now = SystemClock.uptimeMillis();
- Message prevMsg = null ;
- Message msg = mMessages;
- //Get the first message in the current MessageQueue
- if (msg != null && msg.target == null ) {
-
- do {
- prevMsg = msg;
- msg = msg.next ;
- } while (msg != null && !msg.isAsynchronous());
- }
- if (msg != null ) {
- if (now < msg. when ) {
- //The significance of this judgment is that the message should be sent only when it is time to send it, otherwise the loop continues.
- //Calculate the time for the next message. Note that *** is Integer .MAX_VALUE.
- nextPollTimeoutMillis = ( int ) Math. min (msg. when - now, Integer .MAX_VALUE);
- } else { // Time to send a message.
- // Got a message.
- mBlocked = false ;
- if (prevMsg != null ) {
- prevMsg.next = msg.next ;
- } else {
- mMessages = msg.next ;
- }
- msg.next = null ;
- if (DEBUG) Log.v(TAG, "Returning message: " + msg);
- msg.markInUse();
- //Convert the message to be marked as used
- return msg;
- //Return a message to Looper.
- }
- } else {
- // If the obtained Message is null , set the timestamp to -1.
- nextPollTimeoutMillis = -1;
- }
-
- // Process the quit message now that all pending messages have been handled.
- if (mQuitting) {
- dispose();
- return null ;
- }
-
- // If first time idle, then get the number of idlers to run.
- // Idle handles only run if the queue is empty or if the first message
- // in the queue (possibly a barrier) is due to be handled in the future.
- if (pendingIdleHandlerCount < 0
- && (mMessages == null || now < mMessages. when )) {
- pendingIdleHandlerCount = mIdleHandlers. size ();
- }
- if (pendingIdleHandlerCount <= 0) {
- // No idle handlers to run. Loop and wait some more.
- mBlocked = true ;
- continue ;
- }
-
- if (mPendingIdleHandlers == null ) {
- mPendingIdleHandlers = new IdleHandler[Math. max (pendingIdleHandlerCount, 4)];
- }
- mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
- }
-
- // Run the idle handlers.
- // We only ever reach this code block during the first iteration.
- for ( int i = 0; i < pendingIdleHandlerCount; i++) {
- final IdleHandler idler = mPendingIdleHandlers[i];
- mPendingIdleHandlers[i] = null ; // release the reference to the handler
-
- boolean keep = false ;
- try {
- keep = idler.queueIdle();
- } catch (Throwable t) {
- Log.wtf(TAG, "IdleHandler threw exception" , t);
- }
-
- if (!keep) {
- synchronized (this) {
- mIdleHandlers.remove(idler);
- }
- }
- }
-
- //Reset the idle handler count to 0 so we do not run them again.
- pendingIdleHandlerCount = 0;
-
- // While calling an idle handler, a new message could have been delivered
- // so go back and look again for a pending message without waiting.
- nextPollTimeoutMillis = 0;
- }
- }
As you can see, when MessageQueue retrieves a message (calls next()), it will enter an infinite loop until a message is retrieved and returned. This is why Looper.loop() will wait at queue.next(). So, how is a Message added to the MessageQueue? To find out the truth, we need to investigate the mHandler.post() method. What exactly does the Handler do with the Message? The Handler's post() series of methods ultimately call the following method: - private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
- msg.target = this;
- //Assign a value to the target of Message here.
- if (mAsynchronous) {
- msg.setAsynchronous( true );
- //If it is asynchronous, mark it as asynchronous
- }
- return queue.enqueueMessage(msg, uptimeMillis);
- //This method adds the Message to the thread's MessageQueue.
- }
Next, let's take a look at what enqueueMessage() of MessageQueue does. - boolean enqueueMessage(Message msg, long when ) {
- if (msg.target == null ) {
- //If there is no Handler call, an exception will be thrown.
- throw new IllegalArgumentException( "Message must have a target." );
- }
- if (msg.isInUse()) {
- //Cannot use a Message that is already in use.
- throw new IllegalStateException(msg + " This message is already in use." );
- }
-
- synchronized (this) {
- //Lock the MessageQueue and add messages to it.
- if (mQuitting) {
- //If the MessageQueue is marked as exited, return.
- IllegalStateException e = new IllegalStateException(
- msg.target + " sending message to a Handler on a dead thread" );
- Log.w(TAG, e.getMessage(), e);
- msg.recycle();
- return false ;
- }
-
- msg.markInUse();
- //Switch the usage status of Message to unused.
- msg. when = when ;
- //We set the delay time for sending.
- //After the following logic, the Message will be "stored" in the MessageQueue.
- //In fact, the way Message is stored in MessageQueue is,
- //It uses a single linked list structure that points backwards one by one using Message.next to store.
- //For example: A.next = B, B.next = C...
- Message p = mMessages;
- //Try to get the current Message
- boolean needWake;
- if (p == null || when == 0 || when < p. when ) {
- // If it is null , it means it is the first item.
- msg.next = p;
- mMessages = msg;
- //Set the current Message to the incoming Message, that is, as the first message.
- needWake = mBlocked;
- } else {
-
- needWake = mBlocked && p.target == null && msg.isAsynchronous();
- Message prev;
- //When it does not meet the conditions to be the first message, put it on the first page through the following step-by-step transformation.
- //This will "store" the Message in the MessageQueue.
- for (;;) {
- prev = p;
- p = p.next ;
- if (p == null || when < p. when ) {
- break;
- }
- if (needWake && p.isAsynchronous()) {
- needWake = false ;
- }
- }
- msg.next = p;
- prev.next = msg;
- }
-
-
- if (needWake) {
- nativeWake(mPtr);
- }
- }
- return true ;
- }
So far, we have revealed the hidden secrets of Looper, Handler, and MessageQueue. Another question? Maybe you have noticed that Handler can be used directly in the main thread without Looper.prepare() and Looper.loop(). Why is this possible? According to the previous analysis, Looper.prepare() and Looper.loop() must exist in the main thread. In this case, why is the main thread not blocked by loop()? Let's take a look at ActivityThread to find out what's going on. - //This main() method can be considered the starting point of the Android application
- public static void main(String[] args) {
- .
- .
- .
- Looper.prepareMainLooper();
- //The main function is similar to the Looper.prepare () we usually call
-
- ActivityThread thread = new ActivityThread();
- //Create an instance of this class
- thread.attach( false );
-
- if (sMainThreadHandler == null ) {
- sMainThreadHandler = thread.getHandler();
- //The key point! Here we get the Handler that handles the main thread.
- }
-
- if ( false ) {
- Looper.myLooper().setMessageLogging(new
- LogPrinter(Log.DEBUG, "ActivityThread" ));
- }
-
- // End of event ActivityThreadMain.
- Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
- Looper.loop();
- //Start the loop. As you can see, the main thread is essentially blocked!
- .
- .
- .
- }
Note that ActivityThread does not inherit Thread. Its Handler is a private inner class H.class that inherits Handler. In handleMessage() of H.class, it receives and executes various life cycle status messages in the main thread. The 16ms drawing of the UI is also implemented through the Handler. In other words, all operations in the main thread are performed between Looper.prepareMainLooper() and Looper.loop(). In other words, they are performed in the main Handler. Summarize - In Android, Thread is initialized when it is created, and the current thread is used as the parent thread and inherits some of its configurations.
- When a Thread is initialized, it will be added to the ThreadGroup of the specified/parent thread for management.
- Thread is actually started by a native function.
- In the inter-thread communication of Android, you need to create a Looper first, that is, call Looper.prepare(). In this process, it will automatically depend on the current Thread and create a MessageQueue. After the previous step, you can create a Handler. By default, the Handler will automatically depend on the Looper of the current thread, and thus depend on the corresponding MessageQueue, so you know where to put the message. MessageQueue implements a single linked list structure to cache Message through Message.next. The message needs to be sent to the Handler for processing, and Looper.loop() must be called to start the thread's message pumping loop. The loop() is a *** loop inside, which is blocked on the next() method of MessageQueue, because the next() method is also a *** loop inside, until a message is successfully extracted from the linked list and returned. Then, continue processing in the loop() method, mainly sending the message to the target Handler. Then enter the next loop and wait for the next message. Due to this mechanism, the thread is equivalent to being blocked in the loop().
After the above disclosure, we have learned about the secrets of threads and their communication. After mastering these, I believe that in the future development process, we can use threads with clear ideas and absorb the essence of Android's design process. |