preface

Hello, I’m Fang Mu, a programmer working in The Imperial City. This article has been included in the “Interviewer Dad” series, welcome to the big brothers

[Interviewer dad] Tell me about View drawing?

[Interviewer dad] Nagging Android event distribution?

Welcome to pay attention to my wechat public number “Fangmu Rudy”, can be the first time to receive my updates, which not only have technical dry goods, but also recorded a north drift programmer struggle up bit by bit ~

Familiar shadows

A dark shadow approached me slowly

He walked slowly, each step firm and steady, like a heavy drumstick on my heart. Sparsely on the head of the shining high P light, sharp eyes as if through my guilty…

Lean in. He’s sitting across from me

“For an interview?”

“For… The rightness”

“Well, you know Handler, right? Tell me about it.”

What is Handler?

Handler is an Android message processing mechanism. It is bound to Looper and MessageQueue and can be used to switch threads. Often used to receive data from child threads and update the UI in the main thread

Handler thread communication principle

You just said that Handler can switch threads, how does that work?

“Thread switching” is actually a kind of “thread communication”. In order to ensure that the main thread does not block, we often need to perform some time-consuming tasks in the child thread, and then notify the main thread to react accordingly. This process is called inter-thread communication.

In Linux, there is a method of interprocess communication called message queuing. In simple terms, when two processes want to communicate, one process puts a message in the queue, and the other process reads a message from the queue, so the two processes can communicate.

Handler is based on this design. In Android’s multi-threaded system, each thread has its own message queue and can start an infinite loop to read messages from the queue.

When thread B wants to communicate with thread A, it only needs to send A message to the message queue of A, and the event loop of A will read the message to realize the communication between threads

Yoooh, nice. You mentioned event loops and message queues. How are they implemented?

Android’s event loops and message queues are implemented using the Looper class

Looper.prepare() is a static method. It builds a Looper and creates a MessageQueue as a member variable of the Looper. MessageQueue is the queue that holds messages

When looper.loop () is called, an infinite loop is opened inside the thread to continuously read messages from the MessageQueue. This is called the event loop

Each Handler is bound to a Looper, which contains the MessageQueue

So where is this Looper stored?

Looper is stored in the thread. But how to store Looper in a thread introduces another important aspect of Android’s messaging mechanism – ThreadLocal

We mentioned earlier. The looper.prepare () method creates a Looper. One thing it does is put a Looper inside the thread’s local variable ThreadLocal.

// Looper.java private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() ! = null) { throw new RuntimeException("Only one Looper may be created per thread"); } // sThreadLocal is a static object of type ThreadLocal<Looper> sthreadLocal. set(new Looper(quitAllowed)); }Copy the code

So what is a ThreadLocal?

ThreadLocal is also called a local variable of a thread. Its biggest magic is that a single instance of ThreadLocal can fetch different values by calling the get method in different threads. Here’s an example of this usage:

Fun main() {val threadLocal = threadLocal <Int>() threadLocal.set(100) Thread {threadLocal.set(20) println(" child Thread 1 ${threadlocal.get ()}")}.start() ${threadlocal.get ()}"); ${threadLocal.get()}")} ${threadLocal.get()}")Copy the code

The core of a ThreadLocal is the set method, which can be summed up in one sentence:

Threadlocal. set can turn an instance into a member variable for the thread

Take a look at the source code

Public void set(T value) {Thread T = thread.currentThread (); ThreadLocalMap map = getMap(t); // ③ Put the value into the map. If the map is empty, create a map. = null) map.set(this, value); else createMap(t, value); }Copy the code

The method is very simple, according to the current thread to get a map of the thread, and then put value into the map, to achieve the purpose of value into the thread member variable

Multiple Theadlocal turns multiple variables into member variables for the thread. Threads are then managed using a ThreadlLocalMap, with the key being a threadLocal

Knowing the secret of its set method, the get method is easy

Thread T = thread.currentThread (); ThreadLocalMap Map = getMap(t); if (map ! = null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e ! = null) { @SuppressWarnings("unchecked") T result = (T)e.value; // return result; } } return setInitialValue(); }Copy the code

Similar to the set method, except that one writes value to the map and one reads value from the map. hem

Ok, so ThreadLocal is as simple and straightforward as that. So why do you use ThreadLocal as a setup and fetch tool for Looper?

Since Looper is to be placed in threads, each thread only needs one event loop and only one Looper. The event loop is an infinite loop, and extra event loops are meaningless. Threadlocal. set sets Looper to a member variable of the thread

To make it easier to get Looper from different threads, Android provides a static object, looper.sthreadLocal. This is done by calling sthreadLocal. get inside the thread to get the Looper object for the thread

To sum up, using ThreadLocal as the setup and retrieval tool for Looper is quite convenient and reasonable

Ok, so you just said Looper is an infinite loop, right? If there are no messages in the queue, will this infinite loop “idle”?

Of course not! If there is no message in the event loop to process but still execute the loop, it is a meaningless waste of CPU resources! Android doesn’t allow this

To solve this problem, there are two native methods, nativePollOnce and nativeWake, in MessageQueue.

NativePollOnce means to do a poll to find out if there are any messages that can be processed, and if there are none, the thread is blocked, freeing up CPU resources

NativeWake wakes up the thread

So the timing of the invocation of these two methods is obvious

// MessageQueue. Java Boolean enqueueMessage(Message MSG, long when) {if (needWake) {nativeWake(mPtr); }...}Copy the code

In the MessageQueue class, the enqueueMessage method is used to queue messages, and if the thread is blocked, nativeWake is called to wake it up

// MessageQueue. Java Message next() {··· nativePollOnce(PTR, nextPollTimeoutMillis); ...}Copy the code

The next() method is used to fetch the message. Call nativePollOnce() before fetching to query whether there are any messages that can be processed, and block the thread if there are none. Wait for the message to join the queue.

Well, it looks like you noticed some of the boundary handling of the Looper loop. Since Looper is an infinite loop, why doesn’t it lead to ANR?

First of all, let’s make the concept clear. ANR is an exception that is thrown when an application fails to respond to an event within a specified period of time.

The typical example is to perform time-consuming tasks in the main thread. ANR is thrown when the main thread is busy with time-consuming tasks and cannot respond to a touch event within 5s.

But the Looper loop is the cornerstone of the event loop, which is what Android uses to process individual events. Normally, touch events are added to the loop to be processed. But if the previous event takes too long, and the next event waits too long beyond a certain amount of time, then ANR is generated. So Looper infinite loop is not the cause of ANR.

The message queue

Okay, so this little trap didn’t mislead you. How do you sort messages in a message queue?

This depends on the enqueueMessage method of the MessageQueue

EnqueueMessage is the enqueueing method for messages. When the Handler makes inter-thread communication, it calls sendMessage to send the message to the message queue of the receiving thread, and the message queue calls enqueueMessage to enqueue the message.

// MessageQueue.java boolean enqueueMessage(Message msg, Long when) {synchronized (this) {MSG. When = when; // ② mMessages is the header pointer to the list, p is the sentry pointer Message p = mMessages; boolean needWake; 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; / / (3) to traverse the list more when get into position if (p = = null | | the when < p.w hen) {break; } if (needWake && p.isAsynchronous()) { needWake = false; } next = p; next = p; prev.next = msg; } if (needWake) { nativeWake(mPtr); } } return true; }Copy the code

There are 3 steps to join the team:

① Bind the joining time to the when attribute

(2) Traverse the list and find the insert position by comparing when

(3) Insert MSG into the list

That’s how the messages are sorted

Ok, so if I have a message and I want it to be executed first, how do I increase its priority?

Given the last question, the most obvious thing to think about is modifying the When property of Message. That’s one way to do it, but Android gives us a much simpler way to do it scientifically, with asynchronous messaging and synchronization barriers.

In Android’s messaging mechanism, messages are divided into synchronous messages, asynchronous messages, and synchronous barriers. (Yes, synchronization barriers are special messages with a target attribute of NULL). Usually we call sendMessage to send a synchronous message. Asynchronous messages need to be used in conjunction with synchronous barriers to increase the priority of messages.

Synchronization barriers are simple to understand. A synchronization barrier is a special type of message. When an event loop detects a synchronization barrier, instead of fetching messages one by one based on the value of when, it traverses the queue to find an asynchronous message to fetch and execute.

This particular message acts as a flag in the message queue, and when the event loop detects it, it changes its behavior and looks for asynchronous messages instead. It appears to act as a barrier to synchronizing messages. So it’s figuratively called a synchronization barrier.

The source code implementation is very, very simple:

//MessageQueue. Java Message next() {··· · // if (MessageQueue! = null && MSG. Target == null) {// ② Fetch the asynchronous message do {prevMsg = MSG; msg = msg.next; } while (msg ! = null && ! msg.isAsynchronous()); }...}Copy the code

Well, you know what I mean. So what happens if I insert a synchronization barrier and I don’t remove it?

Synchronization barriers are used to “block” synchronous messages and process asynchronous messages. If the synchronization barrier is not removed, the asynchronous messages in the message queue will be processed one by one until the asynchronous message is finished. If there are no asynchronous messages left in the queue, the thread will block and the synchronous messages in the queue will never be executed. So the synchronization barrier needs to be removed in time.

Do you know where sync barriers can be used?

The core purpose of a synchronization barrier is to increase Message priority and ensure that messages are processed first. Android uses the view drawing to avoid a lag. You can see the previous summary of view drawing ~

Why does Handler have a memory leak? What can be done about it?

Memory leaks are ultimately the result of lifecycle “misalignments” : an object that is supposed to be recycled in a short lifetime is referred to by a long-lifetime object, making it unrecyclable. Handler memory leaks are actually caused by inner classes holding references to external classes. There are two forms of formation:

(1) Anonymous inner classes hold references to external classes

class Activity {
    var a = 10
    fun postRunnable() {
        Handler(Looper.getMainLooper()).post(object : Runnable {
            override fun run() {
                [email protected] = 20
            }
        })
    }
}
Copy the code

When a Handler sends a message, the message.target property is the Handler itself. The message is sent to the message queue and is held by a thread, which is an extremely “long” lifetime object, causing the activity to fail to be reclaimed in time and causing a memory leak.

The solution is to remove the Runnable in time for the activity deStory

(2) Non-static inner classes hold external class references

Public void handleMessage(Message MSG) {Override public void handleMessage(Message MSG) {switch (msg.what) { } } }Copy the code

The solution is to use a static inner class and change the external reference to a weak reference

Private static class AppHandler extends Handler {// WeakReference<Activity> Activity; private static class AppHandler extends Handler {// WeakReference<Activity> Activity; AppHandler(Activity activity){ this.activity = new WeakReference<Activity>(activity); } public void handleMessage(Message message){ switch (message.what){ } } }Copy the code

Ok, Handler, Looper and MessageQueue basics, so one last question, do you know HandlerThread and IdleHandler? What are they used for?

HandlerThread is a combination of Handler and Thread. It is essentially a Thread.

As we know, the child thread requires us to manually start the event loop with looper.prepare () and looper.loop (). HandlerThread actually does this for us, it is a thread that implements the event loop. We can do some IO time-consuming operations in this thread.

Idlehandlers, though called Handlers, are a special kind of “message” just like synchronization barriers. Unlike Message, it is an interface

public static interface IdleHandler{
    boolean queueIdle();
}
Copy the code

Idle means Idle. Unlike a synchronization barrier, which increases the priority of asynchronous messages to be executed first, IdleHandler is executed when the event loop is idle.

The word “idle” here mainly refers to two situations

(1) The message queue is empty

(2) The message queue is not empty but all the delayed messages, i.e. MSG. When > now

Using this feature, we can put some unimportant initialization operations in IdleHandler to speed up app startup. Since the drawing of the View is event-driven, we can also add an IdleHandler to the event loop of the main thread as a callback to the completion of the drawing of the View, etc. Note that IdleHandler can be delayed indefinitely if there is a task executing in the main thread

In order to help the brothers better review in the future, I helped you to sort out the brain map of Handler’s interview knowledge points. When reviewing, eat with the brain map to master the basic knowledge of Handler in minutes. “If you need other knowledge brain maps, please follow my official account to get ~”

At the end

This is the third part of the “Interviewer Dad” series, and I will continue to update this series to cover some of the most common interview questions, such as starting an Activity, compiling and packaging processes and optimization, Java basics, design patterns, componentization, and so on. If you don’t want to miss, welcome to like, collect, follow me! This is really important to me!!

I am the wood

A worker struggling to make it up in the Internet world

Try to live, try to move on

Wechat search public number Fangmu Rudy first time to get my updates! It’s not just about technology, it’s about stories and insights

Search it and take me! See you next time ~ bye-bye ~