Android message mechanism Handler, it is necessary to talk about it again

Android message mechanism Handler, it is necessary to talk about it again

In our daily development, we will inevitably use Handler. Although the Handler mechanism is not equivalent to the Android message mechanism, the Handler message mechanism has long been familiar to Android developers and is very important!

[[271774]]

Through this article, you can easily get the answers to the following questions:

  1. What are the principles of Handler, Looper, Message and MessageQueue and the relationship between them?
  2. What is the MessageQueue storage structure?
  3. Why must the child thread call Looper.prepare() and Looper.loop()?

Simple use of Handler

I believe that everyone should know how to use Handler, right? Suppose you are processing a time-consuming task in an Activity and need to update the UI. Let's take a look at how we usually handle it.

  1. override fun onCreate(savedInstanceState: Bundle?) {
  2. super.onCreate(savedInstanceState)
  3. setContentView(R.layout.activity_main3)
  4. // Request network
  5. subThread.start()
  6. }
  7. override fun onDestroy() {
  8. subThread.interrupt()
  9. super.onDestroy()
  10. }
  11. private val handler by lazy(LazyThreadSafetyMode.NONE) { MyHandler() }
  12. private val subThread by lazy(LazyThreadSafetyMode.NONE) { SubThread(handler) }
  13. private class MyHandler : Handler() {
  14. override fun handleMessage(msg: Message) {
  15. super.handleMessage(msg)
  16. // Main thread processing logic, generally a weak reference is needed here to hold the Activity instance to avoid memory leaks
  17. }
  18. }
  19. private class SubThread(val handler: Handler) : Thread() {
  20. override fun run() {
  21. super.run()
  22. // Time-consuming operations such as making network requests
  23. // After the network request is completed, we have to notify the UI to refresh. We will directly consider the Handler processing and ignore other solutions for the time being.
  24. // The first method, generally this data is the content of the request result analysis
  25. handler.obtainMessage(1,data).sendToTarget()
  26. // Second method
  27. val message = Message.obtain() // Try to use Message.obtain() to initialize
  28. message.what = 1
  29. message.obj = data // Generally, this data is the content of the request result analysis
  30. handler.sendMessage(message)
  31. // The third method
  32. handler.post(object : Thread() {
  33. override fun run() {
  34. super.run()
  35. // Handle update operations
  36. }
  37. })
  38. }
  39. }

The above code is very simple. Because network request is a time-consuming task, we open a new thread and notify the main thread to update the UI through Handler after the network request is parsed. We simply use three methods. Careful friends may find that the first and second methods are the same. That is, Handler is used to send a Message object with content. It is worth mentioning that we should use Message.obtain() instead of new Message() to initialize Message as much as possible, mainly because Message.obtain() can reduce memory application.

In response to the suggestions made in the previous article, we will try to post as little source code as possible. You can easily find that all the above methods will eventually call this method:

  1. public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
  2. MessageQueue queue = mQueue;
  3. if (queue == null ) {
  4. RuntimeException e = new RuntimeException(
  5. this + " sendMessageAtTime() called with no mQueue" );
  6. Log.w( "Looper" , e.getMessage(), e);
  7. return   false ;
  8. }
  9. return enqueueMessage(queue, msg, uptimeMillis);
  10. }
  11. private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
  12. msg.target = this;
  13. if (mAsynchronous) {
  14. msg.setAsynchronous( true );
  15. }
  16. return queue.enqueueMessage(msg, uptimeMillis);
  17. }

In the above code, a MessageQueue appears, and finally the MessageQueue#enqueueMessage method is called to enqueue the message. We have to briefly talk about the basic situation of MessageQueue.

MessageQueue

As the name implies, MessageQueue is a message queue, that is, a container for storing multiple messages. It uses a one-way linked list data structure instead of a queue. Its next() points to the next Message element in the linked list.

  1. boolean enqueueMessage(Message msg, long when ) {
  2. // ... some checking code omitted
  3. synchronized (this) {
  4. // ... some checking code omitted
  5. msg.markInUse();
  6. msg.when = when ;
  7. Message p = mMessages;
  8. boolean needWake;
  9. if (p == null || when == 0 || when < p. when ) {
  10. // New head, wake up the event queue if blocked.
  11. msg.next = p;
  12. mMessages = msg;
  13. needWake = mBlocked;
  14. } else {
  15. // Inserted within the middle of the queue. Usually we don't have to wake
  16. // up the event queue unless there is a barrier at the head of the queue
  17. // and the message is the earliest asynchronous message in the queue.
  18. needWake = mBlocked && p.target == null && msg.isAsynchronous();
  19. Message prev;
  20. for (;;) {
  21. prev = p;
  22. p = p.next ;
  23. if (p == null || when < p. when ) {
  24. break;
  25. }
  26. if (needWake && p.isAsynchronous()) {
  27. needWake = false ;
  28. }
  29. }
  30. msg. next = p; // invariant: p == prev. next  
  31. prev.next = msg;
  32. }
  33. // We can assume mPtr != 0 because mQuitting is   false .
  34. if (needWake) {
  35. nativeWake(mPtr);
  36. }
  37. }
  38. return   true ;
  39. }

From the implementation of enqueueMessage(), its main operation is actually the insertion operation of the singly linked list. I will not explain it too much here. We should probably pay more attention to its dequeue operation method next():

  1. Message next () {
  2. // ...
  3. int nextPollTimeoutMillis = 0;
  4. for (;;) {
  5. // ...
  6. nativePollOnce(ptr, nextPollTimeoutMillis);
  7. synchronized (this) {
  8. // Try to retrieve the next message. Return if found.
  9. final long now = SystemClock.uptimeMillis();
  10. Message prevMsg = null ;
  11. Message msg = mMessages;
  12. if (msg != null && msg.target == null ) {
  13. // Stalled by a barrier. Find the next asynchronous message in the queue.
  14. do {
  15. prevMsg = msg;
  16. msg = msg.next ;
  17. } while (msg != null && !msg.isAsynchronous());
  18. }
  19. if (msg != null ) {
  20. if (now < msg.when ) {
  21. // Next message is   not ready. Set a timeout to wake up when it is ready.
  22. nextPollTimeoutMillis = ( int ) Math. min (msg. when - now, Integer .MAX_VALUE);
  23. } else {
  24. // Got a message.
  25. mBlocked = false ;
  26. if (prevMsg != null ) {
  27. prevMsg.next = msg.next ;
  28. } else {
  29. mMessages = msg.next ;
  30. }
  31. msg.next = null ;
  32. if (DEBUG) Log.v(TAG, "Returning message: " + msg);
  33. msg.markInUse();
  34. return msg;
  35. }
  36. } else {
  37. // No more messages.
  38. nextPollTimeoutMillis = -1;
  39. }
  40. //...
  41. }
  42. //...
  43. // While calling an idle handler, a new message could have been delivered
  44. // so go back and look again for a pending message without waiting.
  45. nextPollTimeoutMillis = 0;
  46. }
  47. }

The next() method is actually very long, but we only posted a small part of it. You can see that it is just an infinite loop of for (;;), and a nativePollOnce(long, int) method is called inside the loop body. This is a Native method, which actually blocks the current call stack thread for nextPollTimeoutMillis milliseconds through the Native layer's MessageQueue.

The following is the blocking behavior of different nextPollTimeoutMillis values:

  • Less than 0, blocked until awakened;
  • Equal to 0, no blocking;
  • If it is greater than 0, the longest blocking time is nextPollTimeoutMillis milliseconds. If it is awakened during this period, it will return immediately.

As you can see, the initial value of nextPollTimeoutMillis is 0, so it will not block and will directly fetch the Message object. If the Message object data is not fetched, nextPollTimeoutMillis will be set to -1 directly. At this time, if the condition of less than 0 is met, it will be blocked until another Native method nativeWake(long) is called elsewhere to wake up. If a value is fetched, the obtained Message object will be directly returned.

It turns out that the nativeWake(long) method has a call to the previous MessageQueue#enqueueMessage method, and the call is made during the process of enqueuing a message in the MessageQueue.

Now we know that Handler sends Message, MessageQueue stores it, MessageQueue#enqueueMessage is used to enqueue it, and MessageQueue#next is used to poll messages. This inevitably raises a question: who calls the MessageQueue#next method? Yes, it is Looper.

Looper

Looper plays the role of a message loop in the Android message mechanism. Specifically, it will constantly check whether there are new messages from the MessageQueue through next(). If there are new messages, they will be processed immediately, otherwise the MessageQueue will be blocked there.

Let's take a look at the most important method of Looper: loop():

  1. public   static void loop() {
  2. final Looper me = myLooper();
  3. if (me == null ) {
  4. throw new RuntimeException( "No Looper; Looper.prepare() wasn't called on this thread." );
  5. }
  6. // ...
  7. for (;;) {
  8. Message msg = queue. next (); // might block
  9. if (msg == null ) {
  10. // No message indicates that the message queue is quitting.
  11. return ;
  12. }
  13. //...
  14. try {
  15. // Distribute messages to handler for processing
  16. msg.target.dispatchMessage(msg);
  17. dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
  18. finally
  19. // ...
  20. }
  21. // ...
  22. }
  23. }

The method omits a lot of code and only retains the core logic. As you can see, the Looper object is first obtained through the myLooper() method. If the Looper returns empty, an exception is thrown directly. Otherwise, it enters a for (;;) loop and calls the MessageQueue#next() method to obtain the Message object in turn. If the obtained Message object is empty, it exits the loop() method directly. Otherwise, the Handler object is obtained directly through msg.target and the Handler#dispatchMessage() method is called.

Let's first look at the Handler#dispatchMessage() method implementation:

  1. public void dispatchMessage(Message msg) {
  2. if (msg.callback != null ) {
  3. handleCallback(msg);
  4. } else {
  5. if (mCallback != null ) {
  6. if (mCallback.handleMessage(msg)) {
  7. return ;
  8. }
  9. }
  10. handleMessage(msg);
  11. }
  12. }
  13. private static void handleCallback(Message message) {
  14. message.callback.run();
  15. }

The code is relatively simple. If the Message has a callback set, it directly calls message.callback.run(). Otherwise, it checks whether the message has been initialized.

Let's take a look at the myLooper() method:

  1. public   static @Nullable Looper myLooper() {
  2. return sThreadLocal.get();
  3. }

Let's take a look at what sThreadLocal is:

  1. static final ThreadLocal sThreadLocal = new ThreadLocal();

What is this ThreadLocal?

ThreadLocal

Regarding ThreadLocal, we directly adopt the content in Yan Zhenjie's article.

The first impression of seeing ThreadLocal is that this class is related to threads, which is indeed the case, but it should be noted that it is not a thread, otherwise it should be called LocalThread.

ThreadLocal is used to store data of a specified thread. When the scope of some data is the specified thread and the data needs to run through all execution processes of the thread, ThreadnLocal can be used to store data. When a thread uses ThreadnLocal to store data, only the thread can read the stored data, and other threads except this thread cannot read the data.

Some readers may still not understand the role of ThreadLocal after reading the above paragraph. Let's take an example:

  1. ThreadLocal<Boolean> local = new ThreadLocal<>();
  2. // Set the initial value to true .
  3. local . set ( true );
  4. Boolean bool = local .get();
  5. Logger.i( "The value read by MainThread is: " + bool);
  6. new Thread() {
  7. @Override
  8. public void run() {
  9. Boolean bool = local .get();
  10. Logger.i( "The value read by SubThread is: " + bool);
  11. // Set the value to false .
  12. local . set ( false );
  13. }
  14. }.start():
  15. // The main thread sleeps for 1 second to ensure that the upper sub-thread is executed before executing the following code.
  16. Thread.sleep(1000);
  17. Boolean newBool ​​= local .get();
  18. Logger.i( "The new value read by MainThread is: " + newBool);

There is nothing much to say about the code. You will see the following printed log:

  • The value read by MainThread is: trueThe value read by SubThread is: nullThe value read by MainThread is: true

The first log is unquestionable because the value is set to true, and there is nothing to say about the printed result. For the second log, according to the above introduction, the data stored in a thread using ThreadLocal can only be read by the thread, so the result of the second log is: null. Then the value of ThreadLocal is set to false in the child thread, and then the third log will be printed. The principle is the same as above. The value of ThreadLocal set in the child thread does not affect the data of the main thread, so the print is true.

The experimental results confirm that even for the same ThreadLocal object, the operations of the set() and get() methods of any thread are independent of each other and do not affect each other.

Looper.myLooper()

Let's go back to Looper.myLooper():

  1. static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

Let's see where sThreadLocal is operated.

  1. public   static void prepare () {
  2. prepare ( true );
  3. }
  4. private static void prepare (boolean quitAllowed) {
  5. if (sThreadLocal.get() != null ) {
  6. throw new RuntimeException( "Only one Looper may be created per thread" );
  7. }
  8. sThreadLocal.set ( new Looper(quitAllowed));
  9. }

So you know, this is why you must call Looper.prepare() before using Handler in the child thread.

You may wonder, when I use it in the main thread, I don't require Looper.prepare().

It turns out that we have explicitly called Looper.prepareMainLooper() in ActivityThread:

  1. public   static void main(String[] args) {
  2. // ...
  3. Looper.prepareMainLooper();
  4. // ...
  5. if (sMainThreadHandler == null ) {
  6. sMainThreadHandler = thread.getHandler();
  7. }
  8. //...
  9. Looper.loop();
  10. // ...
  11. }

Let's look at Looper.prepareMainLooper():

  1. public   static void prepareMainLooper() {
  2. prepare ( false );
  3. synchronized (Looper.class) {
  4. if (sMainLooper != null ) {
  5. throw new IllegalStateException( "The main Looper has already been prepared." );
  6. }
  7. sMainLooper = myLooper();
  8. }
  9. }

<<:  SiriOS may be released soon to help Apple's smart home ecosystem

>>:  Simple mode returns to the original intention of mobile QQ new internal beta experience

Recommend

Zhihu content operation manual!

The first draft of this Zhihu content operation m...

8 key points for the fission of 6 distribution activities!

If you only want to see the conclusion (pitfall),...

What is image promotion? How to do image promotion?

In the Internet age, image display ads are becomi...

The correct approach for content operations to conduct topic planning!

In fact, I have wanted to write an article about ...

What is the development prospect of SEO? What can you do after learning SEO?

With the continuous development of the Internet, ...

What is the principle of Amazon's mobile phone 3D visual interface?

This feature, called "Dynamic Perspective&quo...

Dubai expands deployment of driverless shuttle buses

This Saturday, driverless cars were put into tria...

Annoying phone battery life: It’s not the battery’s fault

The functions of the smartphones we use nowadays ...

AE animation tutorial video "The Secret of Keyframes"

Training course content: It covers every aspect o...