Android Handler mechanism: relationship among Looper, Handler, MessageQueue and Message

Keywords: Android

1, Overview

Handler is the mechanism for handling asynchronous messages in Android. To sum up, Looper, handler, MessageQueue and Message are: Looper is responsible for creating a MessageQueue, then entering an infinite loop body, constantly reading messages from the MessageQueue, and then calling back the corresponding Message processing function. The creator of the Message is one or more handlers. After executing a Message, the loop continues.

2, MessageQueue details

Message queue is the queue where messages are stored. What are the messages stored in the queue? Suppose we click a button on the UI interface, and the program happens to receive a broadcast event, how do we deal with these two things? Because a thread can only handle one thing at a time and cannot handle multiple things at the same time, we can't handle button click events and broadcast events at the same time. We can only process them one by one. As long as we process them one by one, there must be a processing order. To this end, Android encapsulates the button click event on the UI interface into a message and puts it into the MessageQueue, that is, the message of the button click event is stacked into the message queue, and then encapsulates the broadcast event into a message and also into the message queue. In other words, a message object represents a thing that a thread needs to process, and a message queue is a pool of messages that need to be processed. Thread will take out the messages in the message queue in turn and process them in turn.

There are two important methods in MessageQueue: enqueueMessage method and next method. The enqueueMessage method is used to put a Message into the Message queue, and the next method is to block a Message from the Message queue.

3, Looper details

The Message queue is just a place where messages are stored. The Looper really makes the Message queue cycle, just like the Message queue is a water tanker, so the Looper is the river that makes the water tanker rotate. If there is no river, the water tanker is a stationary device and is of no use. The Looper makes the MessageQueue move.

Looper is used to loop messages in a thread. By default, when we create a new thread, there is no message queue in this thread. To enable the thread to bind a message queue, we need to rely on Looper: first we have to call the prepare method of Looper, then call Looper's loop method.

(1) prepare() method

public static final void prepare() {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(true));

sThreadLocal is a ThreadLocal object that can store variables in a thread. You can see that an instance of Looper is put into ThreadLocal, and first judge whether sThreadLocal.get is null, otherwise an exception is thrown. This means that the Looper.prepare() method cannot be called twice, and it also ensures that there is only one Looper instance in a thread.

(2) Constructor

The above code implements Looper's constructor. Let's take a look at its code:

private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mRun = true;
        mThread = Thread.currentThread();
}

In the constructor, a message queue MessageQueue is created and assigned to its member field mQueue, so Looper is associated with MessageQueue through the member field mQueue.

(3) loop() method

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);
            }
 
            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.recycle();
        }
}

There are several lines of code above which are the key codes:

1. final Looper me = myLooper();

The myloop () method directly returns the looper instance stored in sThreadLocal. If me is null, an exception will be thrown, that is, the looper method must be run after the prepare method.

final Looper me = myLooper();
if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() 
            wasn't called on this thread.");
        }
public static Looper myLooper() {
    return sThreadLocal.get();
}

2. final MessageQueue queue = me.mQueue;

Get the message queue mQueue in the looper instance. The variable me is the looper bound by the current thread obtained through the static method myloop(), and me.mQueue is the message queue associated with the current thread.

3. for (;;)

Entered the cycle. We found that the for loop does not set the condition for loop termination, so the for loop is an infinite loop.

4. Message msg = queue.next(); // might block

Take out a Message and block if there is no Message. We take a Message from the Message queue through the next method of the Message queue. If there is a Message in the Message queue at this time, the next method will immediately return the Message. If there is no Message in the Message queue at this time, the next method will wait for the Message in a blocking manner.

5. msg.target.dispatchMessage(msg);

The target attribute of msg is the Handler. This code means to let the Handler associated with the Message process the Message through the dispatchMessage method.

6. msg.recycle();

Release the resources occupied by the message.

(4) Looper's main role

1. Bind with the current thread to ensure that a thread has only one Looper instance and only one MessageQueue for a Looper instance.

2. The loop () method continuously fetches messages from the MessageQueue and gives them to the target attribute of the Message, that is, the dispatchMessage of the Handler for processing.

2, Detailed explanation of Handler

(1) Constructor

Before using the Handler, we always initialize an instance, such as for updating the UI thread. We will initialize it directly at the time of declaration, or initialize the Handler instance in onCreate. So let's first look at the construction method of the Handler, see how it connects with the MessageQueue, and how the messages it sends in the child thread (usually sent in non UI threads) are sent to the MessageQueue.

public Handler() {
        this(null, false);
}
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());
            }
        }
 
        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;
    }

There are several lines of code above which are the key codes:

1. public Handler(Callback callback, boolean async)

Handler.Callback is a means to process messages. If this parameter is not passed, the handler's handleMessage method should be overridden. That is, to enable the handler to process messages, we have two methods:

(1) Pass a Handler.Callback object into Hanlder's constructor and implement the handleMessage method of Handler.Callback.

(2) There is no need to pass in the Handler.Callback object to Hanlder's constructor, but you need to override the Handler's own handleMessage method.

 //mHandler is created in the main thread, so the main thread is bound automatically
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what){
                case 1:
                    System.out.println("handleMessage thread id " + Thread.currentThread().getId());
                    System.out.println("msg.arg1:" + msg.arg1);
                    System.out.println("msg.arg2:" + msg.arg2);
                    System.out.println("msg.obj:" + msg.obj.toString());
                    System.out.println("msg.setDate:" + msg.getData().get("QQ"));

                    textview.setText("success");
                    break;
            }
        }
    };

   private Handler mHandler2 = new Handler(new Handler.Callback() {
       @Override
       public boolean handleMessage(@NonNull Message msg) {
           switch (msg.what){
               case 1:
                   System.out.println("handleMessage thread id " + Thread.currentThread().getId());
                   System.out.println("msg.arg1:" + msg.arg1);
                   System.out.println("msg.arg2:" + msg.arg2);
                   System.out.println("msg.obj:" + msg.obj.toString());
                   System.out.println("msg.setDate:" + msg.getData().get("QQ"));

                   textview.setText("success");
                   break;
           }
           return false;
       }
   });

In other words, either way, we have to implement the handleMessage method in some way, which is similar to the design of Thread in Java.

In Java, if we want to use multithreading, there are two ways:

(1) Pass in a Runnable object to the constructor of Thread and implement the run method of Runnable.

(2) There is no need to pass in the Runnable object to the Thread constructor, but override the run method of the Thread itself.

2. mLooper = Looper.myLooper();

First, get the Looper instance saved by the current thread through Looper. Myloop().

3.mQueue = mLooper.mQueue;

Then get the message queue MessageQueue saved in the Looper instance, which ensures that the Handler instance is associated with the MessageQueue in the Looper instance.

(2) sendMessage() method

   public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }
   public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageDelayed(msg, delayMillis);
    }
 public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
 public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

Finally, the sendMessageAtTime is invoked. In this method, we get the MessageQueue directly and then call the enqueueMessage method. Let's look at this method again.

 private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

There are several lines of code above which are the key codes:

1. msg.target = this;

This code binds the target of the Message to the current Handler.

2. queue.enqueueMessage;

The variable queue represents the Message queue bound by the Handler. We put the Message into the Message queue by calling queue.enqueueMessage(msg, uptimeMillis).

It is now clear that the Looper will call the prepare() and loop() methods to save a Looper instance in the currently executing thread. This instance will save a MessageQueue object, and then the current thread enters an infinite loop to constantly read messages sent by the Handler from the MessageQueue. Then call back the dispathMessage method in the Handler that created the message. The source code of the dispatcher message of the Handler is as follows:

public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }

There are several lines of code above which are the key codes:

1. if (msg.callback != null) { handleCallback(msg); }

First, you will judge whether msg.callback exists. Msg.callback is Runnable. If msg.callback exists, it means that the Message is put into the Message queue by executing the Handler's postXXX series methods. In this case, handlecallback (MSG) will be executed. The handlecallback source code is as follows:

private static void handleCallback(Message message) {
        message.callback.run();
}

In this way, we can clearly see that we have executed the run method of msg.callback, that is, the run method of the Runnable object passed by postXXX.

2. else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } }

If we do not put the Message into the Message queue through the postXXX series of methods, msg.callback is null. The code continues to execute, and then we will judge whether the member field mCallback of the Handler exists. mCallback is of type Hanlder.Callback. We mentioned in the Handler constructor above. In the Handler constructor, we can pass an object of type Hanlder.Callback, which needs to implement the handleMessage method. If we pass the Callback object in the constructor, we will let the handleMessage method of Callback process the Message.

3.handleMessage(msg);

If we do not pass in an object of Callback type in the constructor, mCallback will be null. Then we will call the Handler's own hanldeMessage method, which is empty by default. We need to rewrite and implement the method ourselves.

To sum up, we can see that the Handler provides three ways to process messages, and there are pre and post priorities: first, try to let the Runnable passed in postXXX execute, second, try to let the handleMessage method of Callback passed in the Handler constructor process messages, and finally let the Handler's own handleMessage method process messages.

Let's take a look at handleMessage(msg)

  /**
    * Subclasses must implement this to receive messages.
    */
   public void handleMessage(Message msg) {
   }

We can see that this is an empty method. Why? Because the final callback of the message is controlled by us. When we create the handler, we copy the handleMessage method, and then process the message according to msg.what.

private Handler mHandler = new Handler(){
        public void handleMessage(android.os.Message msg){
            switch (msg.what){
            case value:
                break;
            default:
                 break;
            }   
        };
    };

Now that the sendMessage method process has been explained, let's look at the post method.

(3) post() method

mHandler.post(new Runnable()
        {
            @Override
            public void run()
            {
                Log.e("TAG", Thread.currentThread().getName());
                mTxt.setText("yoxi");
            }
        });

Then, you can write the code to update the UI in the run method. In fact, the Runnable does not create any threads, but sends a message. See the source code below:

 public final boolean post(Runnable r){
       return  sendMessageDelayed(getPostMessage(r), 0);
    }
  private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

You can see that in getPostMessage, we get a message object, and then assign the Runable object we created as the callback attribute to this message. Note: to generate a message object, either new or Message.obtain() method can be used; Both are OK, but it is more recommended to use the obtain method, because a message pool is maintained inside the message for message reuse to avoid using new to reallocate memory.

 public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
 public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

Finally, like handler.sendMessage, called sendMessageAtTime, then called the enqueueMessage method, assigning msg.target to handler, and finally adding MessagQueue.

 public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

As you can see, msg. Callback will be executed if both the callback and target of msg have values= If NULL is determined, the handleCallback callback is executed, that is, our Runnable object.

3, Summary

Now that the process has been explained, let's first summarize:

  1. First, loop. Prepare() saves a loop instance in this thread, and then saves a MessageQueue object in this instance; Because Looper.prepare() can only be called once in a thread, there will only be one MessageQueue in a thread.
  2. In the Looper constructor, a message queue MessageQueue is created and assigned to its member field mQueue, so Looper is associated with MessageQueue through the member field mQueue.
  3. Loop. Loop () will let the current thread enter an infinite loop, read the message from the instance of MessageQueue, and then call back the msg.target.dispatchMessage(msg) method.
  4. The Handler construction method will first get the Looper instance saved in the current thread, and then associate it with the MessageQueue in the Looper instance.
  5. The handler's sendMessage method will assign the target of msg to the handler itself, and then add it to the MessageQueue.
  6. When constructing the Handler instance, we will override the handleMessage method, that is, the method finally called by msg.target.dispatchMessage(msg).

4, A picture is worth a thousand words

In this article, we discussed the relationship among Thread, MessageQueue, Looper and Hanlder. We can more vividly understand the relationship between them through the following diagram of conveyor belt.

In the production and life of real life, there are all kinds of conveyor belts. The conveyor belt is covered with all kinds of goods. Driven by the engine roller, the conveyor belt has been rolling forward. New goods are constantly placed at one end of the conveyor belt, and the goods are sent to the other end for collection and processing.

We can regard the goods on the conveyor belt as messages, and the conveyor belt carrying these goods is the Message queue containing messages. The conveyor belt is driven to rotate by the transmitter roller. We can regard the transmitter roller as a Looper, and the rotation of the engine requires power. We can regard the power as a Thread. All operations of all Message loops are based on a Thread. When everything is ready, we just need to press the power switch and the engine will turn. This switch is the loop method of Looper. When we press the switch, we are equivalent to executing the loop method of Looper. At this time, Looper will drive the Message queue to cycle.

What is Hanlder equivalent to in the conveyor belt model? We can think of the Handler as a pipe for putting in and taking out goods: goods are drawn into the conveyor belt along the pipe from one end, and goods are drawn out of the conveyor belt along the pipe from the other end. The operation of placing goods at one end of the conveyor belt is equivalent to calling the sendMessageXXX, sendEmptyMessageXXX or postXXX methods of the Handler, which puts the Message object into the Message queue. When the goods are drawn along the pipeline from the other end of the conveyor belt, we call Hanlder's dispatchMessage method, and finally execute the handleMessage method, in which we complete the processing of the Message.

Relevant video recommendations:
[Android handle interview] how does MessageQueue keep the thread safety of messages added by each thread?
[Android handle interview] how should the looper in the sub thread deal with the performance problem when there is no message?
[Android handle interview] how many handles and looper s does a thread have?

This article is transferred from https://juejin.cn/post/6844904195653386248 , in case of infringement, please contact to delete.

Posted by romeo on Wed, 17 Nov 2021 19:33:40 -0800