An article to get the "Handler Mechanism"

foreword

Everyone must not have a pattern for the Handler mechanism
, so why does the Handler appear so frequently? The interview is also a key knowledge.
Because the Handler is the basis for supporting the operation of the entire Android system, the Android system is essentially driven by events. The core of event processing lies in Handler.

Take a look at the table of contents of this article to help you answer this question

  • What is Handler
  • The structure of Handler
  • Handler's operating model
  • Analysis of common Handler problems

What is Handler

Official explanation: In Android, Handler is a class used to send and process messages. It is mainly used to implement communication between threads and asynchronous message processing mechanism.
To put it bluntly: it is inter-thread communication. To quote
an official sentence in View: the entire view tree is single-threaded. When calling any method on any view, it must always be on the UI thread. If you work on other threads and want to update the state of the view from this thread, you should use Handler.
That is to say, Handler plays a very important role in solving the drawing and updating of View.

Roles in Android

Just two roles:

1. The sender of the message: Send the message to the target thread through the sendMessage() method of the Handler.
2. The processor of the message: process the received message by rewriting the handleMessage() method of the Handler.

Examples of commonly used methods on Android:

  • Update the UI in the main thread: Since the main thread does not allow time-consuming operations, when you need to update the UI after completing some time-consuming operations in the background thread, you can use Handler to switch the work of updating the UI to the main thread.
  • Implementing asynchronous tasks: When it is necessary to execute a task in a background thread and wait for the task to complete before proceeding to the next step, Handler can be used to implement asynchronous task processing.
  • Execution of timed tasks: The execution of timed tasks is realized through the postDelayed() method of Handler, which can execute tasks in the target thread after a specified time delay.

Knowing what Handler can help us?

It solves all the common problems we have in APP. (I will not explain why each problem is, and it will be interspersed in the following introduction)

  • Caton detection
  • ANR monitoring
  • memory leak
  • Use of Looper

Handler member introduction

Let me introduce the main members we have:
Handler, Looper, Message, MessageQueue, ThreadLocal

Handler

Handler is equivalent to a tool class wrapped by several other members.
Why do you say that? Then let me tell you what he can do, listen to whether it is a tool

  • Handler contains Looper, Messager, MessageQueue, and ThreadLocal to receive, manage, and send our messages.
  • Provides the sendMessage() series of methods: sending ordinary messages, delaying messages, and finally calling the queue.enqueueMessage() method to store the messages in the message queue
  • Provides post() series methods: Submit ordinary/delayed Runnable, then encapsulate it into Message, call sendMessage() to store in the message queue
  • Provides the dispatchMessage() series of methods: dispatch messages, execute msg.callback (that is, runnable) first, then mCallback.handleMessage(), and finally handleMessage()

Brothers, isn't this a tool class that provides an external interface and processes messages? Am I right or not.
It is a tool class for processing thread communication that encapsulates message creation, delivery, and processing mechanisms.

Looper

It is the consumer, the consumer of Kaka consuming Message.
Continuous loop execution (Looper.loop), read Message from MessageQueue. Then use dispatchMessage to distribute the message for processing.
There are two common methods:

  • prepare(): Create a message queue
  • loop(): Traversing the message queue, continuously fetching messages from the message queue, waiting if the message queue is empty

The default Looper is our main thread Looper. We can get it through getMianLooper().

val mHandler = Handler(Looper.getMainLooper())

We can also create our own Looper by ourselves.

public class MyHandler extends Handler {
    private static class MyThread extends Thread {
        public Handler mHandler;
        
        @Override
        public void run() {
            Looper.prepare();
            mHandler = new Handler();
            Looper.loop();
        }
    }

    public MyHandler() {
        MyThread myThread = new MyThread();
        myThread.start();
        
        // 等待子线程中的Looper初始化完成
        while (myThread.mHandler == null) {}
        
        // 将子线程中的Handler对象赋给当前Handler
        this.mHandler = myThread.mHandler;
    }
}

Call Looper.prepare() in the run method to initialize the Looper of the child thread, and then call Looper.loop() to make it loop through the messages in the message queue.

Message

It is a message bearer class (that is, an entity class) designed using the Flyweight mode.
The source code is as follows:
insert image description here
Common methods

  • obtain() series: get a message instance
  • recycle(): recycle message instance

The actual shared message queue provides the function of saving and fetching messages. The bottom layer is implemented by a linked list (linked list has advantages in insertion and deletion). There is only one common method:

  • next(): Get the message.
      1. There is a message, and the message expires and can be executed, and the message is returned
      1. There is a message, the message has not expired, enter the time-limited waiting state
      1. No message, enter an indefinite wait state until woken up

To put it simply: it is a container that manages Messages, waiting for Looper to process them

ThreadLocal

In fact, strictly speaking, he is not a member of Handler.
But Handler borrows ThreadLocal to ensure the uniqueness of MessageQueue in the current thread thread.
Why do you say that: look at the source code

public final class Looper {
    ........
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

ThreadLocal stores Looper objects
and MessageQueue is a member variable of Looper

public final class Looper {
    ........
    final MessageQueue mQueue;

So ThreadLocal guarantees the uniqueness of MessageQueue in the thread.
ps; this is also the cause of memory leaks (discussed below)

Handler's operating model

The above mentioned each member, and also said that Handler is a tool class and an appearance.
Then let's see what the brothers are doing inside.
First, let’s take a picture (the old rules)
insert image description here
. After seeing this picture, you should be able to understand the whole process with the introduction of the above members.

  • 1. After the child thread finishes processing the time-consuming process, send the message through sendMessage() and send it through enqueueMessage

The process from sendMessage() to queue.enqueueMessage() in the above figure is
insert image description here
the last two steps: the source code is as follows

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}
  • 2. Looper performs a for loop, and obtains Message through next() for distribution

The source code is as follows: Looper.java can be understood at a glance

public static void loop() {
    .......
    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}

private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // might block
    。。。。。。。

MessageQueue.java

Message next() {
    ......
    for (;;) {
        //阻塞
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
               // Stalled by a barrier.  Find the next asynchronous message in the queue.
               do {
                    prevMsg = msg;
                    msg = msg.next;
               } while (msg != null && !msg.isAsynchronous());
    .........
   }
  • 3. Get the Message through Looper for distribution

Looper.java

@SuppressWarnings("AndroidFrameworkBinderIdentity")
private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
        ......
        msg.target.dispatchMessage(msg);
        ......

Handler.java

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

The process is like this, let's take a look at the details through the following problem analysis.

Analysis of Handler's common problems

See what are the common problems in Handler

What are the other thread communications in Android except Handler?

  • BroadcastReceiver (broadcast receiver): Communication between different components is achieved by sending and receiving system broadcasts.
  • AsyncTask (asynchronous task): used to perform asynchronous tasks on the background thread and update the UI on the main thread.
  • EventBus: is a publish/subscribe event bus that simplifies communication between different components. EventBus can send and receive events between different threads.
  • RxJava: It is an asynchronous programming library based on observer mode, which simplifies multi-threaded programming and asynchronous task processing, and provides rich operators and thread schedulers.

Reasons for Handler memory leaks

Everyone knows that memory leaks are due to the fact that there is no object that has ended its life during the end of its life cycle.
So why does Handler cause memory leaks?
Because he holds the Activity object (the non-static inner class holds the external class reference by default), the Activity is closed when the Handler has not finished processing the task. At this time, because the Handler still holds the Activity, a memory leak is caused. (Such a simple answer is GG)
But the question is coming?
In "An article to get the perfect diagram of the JVM" we said:
We know that the GC's recycling algorithm is based on the reachability analysis algorithm for recycling marks. However, when there is no GC Root to refer to the object, it will be marked.
Then the Handler is not the GC Root, but the Handler class object we created, so if it is not recycled, it must be referenced by a GC Root.

Next, we use the source code to find out which Root refers to the Handler, so that the Activity cannot be recycled.
1. The first step: We send the final message to the Handler.enqueueMessage() method through Handler.sendMessage(). This is also mentioned above.
2. Step 2: Let's look at the source code of Handler.enqueueMessage()

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;  //奥秘在这里
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

It can be seen that we refer this, which is the current Handler, to msg.target
3. Step 3: msg.target is a member variable of Message.

public final class Message implements Parcelable {
    @UnsupportedAppUsage
    /*package*/ Handler target;

4. The fourth step: Needless to say, the Message is in the MessageQueue. And MessageQueue is a member variable of Looper

public final class Looper {
    @UnsupportedAppUsage
    final MessageQueue mQueue;

5. Looper is stored in ThreadLocalMap by ThreadLoacl.

public final class Looper {
    @UnsupportedAppUsage
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    
    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));
    }
}

6. ThreadLocalMap is static, that is, our GC Root (it is said in an article "The Perfect Illustration of JVM")

static class ThreadLocalMap { }

This time everyone knows why Handler has a memory leak problem. Don't answer "non-static inner classes hold outer class references by default".

The principle of Handler sending delayed messages

First of all, the message delay we use Handler is implemented by calling the sendMessageDelayed function, where delayMillis is the milliseconds that need to be delayed.

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

So what is his rationale? It is mainly put into the queue and operated when it is put into the queue, that is, MessageQueue.enqueueMessage and MessageQueue.next(). One in and one out.

  • First of all, he must bring in the delayed information, right?

This is ultimately messageQueue.java in the enqueueMessage method in messageQueue

 boolean enqueueMessage(Message msg, long when) {
     .....
     synchronized (this) {
        ...

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake; // 是否需要同步队列休眠 标记

        // 按照 Message 的 when,有序插入 MessageQueue 中
        // 如果 没接收到消息 || 插入的消息的when==0(立即插入)|| 插入的执行时间 < 当前处理消息的时间点
        // 上面的判断条件,无非就是 判断一个队列的插入几个特殊场景(头、尾)
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // 需要唤醒 && 消息屏障 && 是一个异步消息
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            // 插入节点 和前驱后继节点相连
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }
        
        if (needWake) {
            nativeWake(mPtr);
        }
        ... 
    }

 }

When sendMessage, MessageQueue.enququeMessage() will insert messages into the queue according to the actual execution time order (ie msg.when), and then execute nativeWake() to wake up. Waking up is actually sending the message immediately (so how to achieve the delay?)

  • When a message is sent, a wait is performed through nativePollOnce

We still need to look at the next() method.
I will simplify the next() method. You can read
MessageQueue.java against the source code

 @UnsupportedAppUsage
 Message next() {
     for (;;) {
         .....
         //借助native的epoll命令阻塞一段时间,超时之后自动恢复
         nativePollOnce(ptr, nextPollTimeoutMillis);
         synchronized (this) {
         .....
             if (msg != null) {
                    /** 2、计算Message执行时间与当前时间的差值,即延迟时间 */
                    if (now < msg.when) {.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        /** 3、无需延迟,也就是达到这个when的时间了,立即返回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();
                        return msg;
                    }
                } else {
                    nextPollTimeoutMillis = -1;
                }
               .....
               if (pendingIdleHandlerCount <= 0) {
                    /** 4、没有IdelHandler,重新进行循环 */
                    mBlocked = true;
                    continue;
                }
         }
     }
 }

1. You can see that the nativePollOnce() is used to block here.
2. The blocking time can be seen to be calculated by the following, that is

nextPollTimeoutMillis = (int)Math.min(msg.when - now, Interge.MAX_VALUE);

3. If this time is reached, or there is no delay time. Then directly return to the Message and send it away
4. If there is still a delay, then assign a value to nextPollTimeoutMillis, and nativePollOnce to block

5. Let’s talk about this IdelHandler later .

  • MessageQueue.enququeMessage() mainly uses when to sort messages into MessageQueue.
  • Why sort? For the following next time, the subsequent messages must not exceed the delay time when deblocking, because the previous messages are shorter than the subsequent messages
  • In MessageQueue.next(), the epoll command is blocked by calculating nextPollTimeoutMillis.

What is nativePollOnce()

nativePollOnce() is the underlying implementation of the next() method. It is a Native method used to actually perform the operation of taking out the next message from the message queue.

nativePollOnce() will call the underlying epoll function (Linux event-driven library) to listen and wait for events on the file descriptor. When an event occurs, nativePollOnce() will return a non-negative integer value indicating the number of times the event occurred; when no event occurs, nativePollOnce() will return a negative number. Through this return value, the next() method can determine whether an event has occurred and process it accordingly.

Why make an endless loop?

The main thread does enter an infinite loop state through Looper.loop(), and because of this, the main thread will not be like the thread we usually create. When the executable code ends, the thread life cycle ends.
This can ensure that our main thread will always survive, and we can also imitate this method to meet the needs of thread keeping alive.

Why does the infinite loop of Handler's For not return to ANR?

First of all, Handler is non-blocking without messages, so it will not ANR, then see why it is non-blocking.
Just now we analyzed that Looper.loop() is the execution process of an infinite loop method. But the UI thread is not blocked, but can perform various gesture operations. There is no ANR either. This is because in the next method of MessageQueue, you can see:

@UnsupportedAppUsage
Message next() {
    .....
    // native 方法:当调用此方法时,主线程会释放CPU资源进入休眠状态
    // 直到下一条消息到达之前或者有事务发生
    // 通过往 管道 写端写入数据来唤醒主线程工作nativeWake(mPtr);
    // 这里采用 epoll 机制:放弃CPU主权交给别的线程执行,因此只会阻挂起但不会阻塞主线程
    nativePollOnce(ptr, nextPollTimeoutMillis);
    
    // 当Looper 被唤醒后,会继续往下执行
   synchronized (this) {
       if (msg != null) {
                if (now < msg.when) {
                    // 如果上面检索出了消息,但是还没有到可以执行的时间
                    // 则更新 nextPollTimeoutMillis 也就是下一次循环需要阻塞的时间值(动态传递剩余时间,不剩余即可唤醒执行)
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 找到了需要处理的消息
                    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();
                    return msg;
                }
            } else {
                // 如果没有找到消息,即队列为空,looper将进入永久休眠,知道新消息到达
                nextPollTimeoutMillis = -1;
            }
   }
}

When there is no message in the MessageQueue of the main thread, it is blocked in the nativePollOnce() method in MessageQueue.next(). At this time, the main thread will release CPU resources and go to sleep until new messages arrive. Therefore, the main thread is in a dormant state most of the time and does not consume a lot of CPU resources.
The epoll mechanism of Linux is used here, which is an IO multiplexing mechanism that can monitor multiple file descriptors at the same time. When a file descriptor is ready (read or write ready), the corresponding program is immediately notified to read or write. The operation gets the latest news, and then wakes up the waiting thread.

What is the difference between post and snedMessage methods for sending messages?

  • The post method sends a Runnable object, but it will be encapsulated into a Message object in the end. The post method does not carry the identifier of the message, so it is impossible to distinguish different messages in the Handler.
  • The message sent by a method like sendMessage is directly a Message object. And distinguish different messages by identifiers when processing messages.

In general, the post method is suitable for sending simple tasks or code blocks, while the sendMessage method is suitable for sending messages that need to be processed and can carry additional data. Which method you choose to use depends on your specific needs and circumstances.
In fact, I just think that the post is easier to write, and there is no difference.

Why can the main thread new Handler? If you want to prepare for the new Handler in the child thread?

The main thread can be new Handler because in Android, each application is a single-threaded model, and the main thread is the UI thread, which is used to handle user interface-related operations. The Handler is used to process messages and Runnable objects in combination with MessageQueue, thereby realizing asynchronous message processing.

If you want to create a new Handler in a child thread, you need to pay attention to the following preparations:

  • Before creating a Handler in a child thread, you need to call the Looper.prepare() method to prepare the Looper object. Looper is used to manage MessageQueue and Handler associated with threads, and only one Looper can exist in a thread.
  • Before creating the Handler, you need to call the Looper.loop() method to start the message loop mechanism. Looper.loop() will cause the thread to enter an infinite loop, continuously get messages from MessageQueue and distribute them for processing.
  • When creating a Handler in a child thread, you need to pay attention to the Looper object passed in the constructor of the Handler. For example, you can use new Handler(Looper.myLooper()) to get the Looper object of the current thread.

It should be noted that after the Handler is created in the child thread, the UI can be updated by sending messages and Runnable objects through the Handler, but the UI update operation cannot be performed directly in the child thread. If you need to update the UI, you can send a message to the main thread through the Handler, and then handle UI update-related operations in the main thread.

How many Loopers and Handlers does a thread have? How to guarantee it?

  • A thread can only have one Looper.

Looper is stored in ThreadLocal to ensure that each thread has only one Looper. ThreadLocal is a mechanism used in Java to implement thread local variables (Thread Local Variables), which provides each thread with an independent copy of variables, so that each thread can independently change its own copy without affecting other threads .

  • A thread can have multiple Handlers, but multiple Handlers use the same Looper.

You can see that msg.target = this; gives the current Handler to target. thus marking the Handler

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

How should it be created when using Message?

  • Created using the obtain() method of Message
  • If it is directly new, it is easy to cause memory jitter.

How is thread safety ensured internally?

  • There can be multiple Handlers to add data to MessageQueue (each Handler may be in a different thread when sending a message)
  • The method of adding a message, enqueueMessage(), has a synchronize modification, and the method of getting a message, next(), also has a synchronize modification.
  • This is also the reason why the delay time is not so accurate, and synchronize is still consumed efficiently.

Why is the Handler in kotlin obsolete?

In Kotlin, Handler is marked as obsolete because Kotlin recommends using a more modern and safe asynchronous programming mechanism

  • For example, Coroutines. Using coroutines can better manage asynchronous tasks, and can provide cleaner code and better performance.
  • However, the traditional Handler mechanism has some problems when processing asynchronous tasks, such as updating UI elements in the UI thread, but processing time-consuming tasks in other threads will cause UI freezes and may cause memory leaks and other problems.

Therefore, Kotlin recommends that developers use coroutines instead of Handlers to better handle asynchronous tasks.

What is IdleHandler

To put it bluntly, IdleHandler is a mechanism provided by the Handler mechanism, which allows us to perform tasks when it is idle during the Looper event loop. It is stored in the mPendingIdleHandlers queue.
So how to understand this mechanism?
Simply put: IdleHandler will be executed when MessageQueue currently has no messages that need to be processed immediately.

public interface IdleHandler {
    boolean queueIdle();
}

Let's see how to use it first?
Returning false means that the callback will be removed after execution, and returning true means that the IdleHandler is always active and will be called back as long as it is idle.

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override
    public boolean queueIdle() {
        //空闲时处理逻辑

        return false;
    }
});

MessageQueue.java

Message next() {
    .....
    for (int i = 0; i < pendingIdleHandlerCount; i++) {
      final IdleHandler idler = mPendingIdleHandlers[i];
      mPendingIdleHandlers[i] = null; 
 
      boolean keep = false;
      try {
        keep = idler.queueIdle();
      } catch (Throwable t) {
        Log.wtf(TAG, "IdleHandler threw exception", t);
      }
 
      if (!keep) {
        synchronized (this) {
          mIdleHandlers.remove(idler);
        }
      }
    }
    .......
}

The general process is as follows:

  1. If the Message obtained in this loop is empty, or the Message is a delayed message and the specified trigger time has not yet arrived, then the current queue is considered to be idle.
  2. Then it will traverse the mPendingIdleHandlers array (the elements in this array will go to mIdleHandlers every time) to call the queueIdle method of each IdleHandler instance.
  3. If this method returns false, then this instance will be removed from mIdleHandlers, that is, when the queue is idle next time, it will not continue to call back its queueIdle method.

After IdleHandler is processed, nextPollTimeoutMillis will be set to 0, that is, the message queue will not be blocked. Of course, it should be noted that the code executed here should not be too time-consuming, because it is executed synchronously. If it is too time-consuming, it will definitely affect the subsequent message execution.

Usage scenarios of IdleHandler

  • LeakCanary's memory leak detection does not perform garbage collection and some analysis after the onDestry method is executed, but uses IdleHandler to perform these operations when it is idle, and try not to affect the operation of the main thread.
  • If you want to add other Views that depend on this View after the drawing of a View is completed, of course, this can also be achieved with View.post(), the difference is that the former will be executed when the message queue is idle
  • Send an IdleHandler that returns true, and make a certain View blink in it, so that when the user is in a daze, the user can be induced to click on the View, which is also a cool operation

Summarize

Handler has a lot of content and is intricate. But not only is it useful at work, it can enhance our ability to solve problems. And it is also the most important thing in the interview.
Handler must go to the source code to understand. It is mainly the methods of the several key classes mentioned above.
MessageQueue.next() and MessageQueue.enququeMessage() are two core methods that must be looked at.

Guess you like

Origin blog.csdn.net/weixin_45112340/article/details/131905663