Android source code learning-Handler mechanism and its six core points

Foreword:

This article belongs to the article in the Android source code exploration column. The list of all articles in the column is linked below. Welcome to read.

Android source code exploration icon-default.png?t=M1L8https://blog.csdn.net/rzleilei/category_6506586.html?spm=1001.2014.3001.5482

1. Introduction to the Handler mechanism

1.1 What is Handler?

Handler is a very commonly used mechanism in Android, which is mainly used for communication between threads.

1.2 Why should you understand the Handler principle?

I was chatting in a group before and said that handlers were often asked in interviews, and then I was sprayed, saying that now it is MVVM, ViewBinding and other architectures, who still uses handlers. Indeed, there are more and more frameworks encapsulated on Android now, which makes it easier and easier for us to use, and there are fewer and fewer scenarios for using Handler. But less usage doesn't mean it's useless. These existing frameworks, and even the foreseeable future frameworks, are based on the Handler mechanism.

For example, we ordinary people drive, maybe as long as we know how to drive, we don't need to understand the principle of the car. But if you want to be a race car driver, you still need to have some understanding of the mechanics and principles of how a car works.

Finally, what are the benefits of understanding the handler principle?

1. The handler mechanism is very classic, and it can also be used depending on the scene in the process of typing code.

2. In fact, in addition to ordinary synchronous messages, handlers also include barrier messages and asynchronous messages. We can also use them according to the scene.

3.handler also provides the idle mechanism IdelHandler.

4. The handler can help us find out where the code is causing the freeze.

The above points will be explained in detail in the following articles.

1.3 What is the role of Handler in the interview?

At the same time, Handler is also an Android mechanism that is often asked in interviews.

I concluded that there are about six points that will be frequently asked. These six technical points are specially marked in red and belong to the technical points that are easy to be asked in the interview.

For example: technical point 1

Second , the Handler principle overview

As shown in the following figure: The mechanism of Handler in Android is to obtain a Handler object in the child thread, and send tasks through this Handler object like MessageQueue. An infinite infinite loop is opened in the main thread, and the Message task is continuously retrieved from the MessageQueue. If it is retrieved, the task in the Messagez is executed. If it is not retrieved, it enters the sleep state.

 Several important objects involved

Handler: Generally created in the main thread, holding a Looper of the main thread, responsible for message distribution to the main thread.

Message: It can be understood as task, the unit of task execution.

MessageQueue: Inside is a singly linked list, which stores Message tasks.

Looper: Responsible for processing Message tasks, and its loop method opens an infinite loop in the main thread.

3. The process of adding Message

3.1 Add the writing of Meesage

We can see many ways to add message, but in the end, the sendMessageDelayed method is called.

The general spellings are as follows:

Writing method 1: sendMessageDelayed(Message msg, long delayMillis)

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

Writing method 2: sendMessage(Message msg)

 public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
    }

In fact, the sendMessageDelayed method is called.

Writing three:

mHandler.post(new Runnable() {
                @Override
                public void run() {
                   //do something 
                }
            });

Actually, let's go in and take a look.

sendMessageDelayed(getPostMessage(r), 0);

Look at the getPostMessage method again:

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

All right. In fact, it is also to build a Message, and then sendMessage out.

3.2 sendMessageDelayed方法

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

The second parameter passed in when calling sendMessageAtTime is the execution time.

This execution time is calculated from the current time + delay time.

Here is a technical point 1: Will modifying the mobile phone time affect the execution of Message? The current use is SystemClock.uptimeMillis(), which refers to the non-deep sleep time of the system after the mobile phone is turned on, not the mobile phone time. Therefore, modifying the current time on the mobile phone will not affect the execution of Message. Generally, we can also use this value to get how long the phone is powered on.

3.3 sendMessageAtTime

1. A MessageQueue judgment is made in sendMessageAtTime.

 public boolean sendMessageAtTime(@NonNull 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);
    }

3.4 enqueueMessage method

  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);
    }

Add Message to queue

3.5 enqueueMessage method

1. The code here looks a bit too much and messy. But the core logic is to insert the current message node into the singly linked list according to the latitude of the execution time from front to back. Technical point 2: In what way is the linked list arranged? execution time sequence

2. There is a parameter when in Message, which will be assigned when it is added to the linked list, and the execution time is recorded.

3. At the same time wake up sleep through nativeWake. Why hibernate, we will talk about in the fourth chapter below, generally when there is no message to be executed, it will enter hibernation to release CPU resources.

boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }

        synchronized (this) {
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }

            if (mQuitting) {
                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();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                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;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

At this point, the message is inserted into the MessageQueue, and the insertion process is completed.

Fourth, the process of Message being executed

4.1 Call Looper.prepare() to bind with the current thread.

Generally, we do not need to call the prepare method, because after the application starts, in the main method of ActivityThread, the Looper.prepare method has been called for us.

  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));
    }

The core logic of prepare is to bind with the current thread through ThreadLocal. It is guaranteed that a Looper will only bind to only one Thread.

4.2 Looper.loop starts an infinite loop

Call the Looper.loop method to start an infinite loop. A check is made here, and if Looper is not bound to the current thread, an exception will be thrown.

public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        ...不重要代码忽略

        for (;;) { //启动无限循环
            if (!loopOnce(me, ident, thresholdOverride)) {
                return;
            }
        }
    }

4.3 Get the message and try to execute it

1. It will call MessageQueue.next() to try to get the message, if it can't get it, it will be blocked. We will analyze it in the next chapter.

2. If the message is not obtained and returned, the application is in the exit state, so the loop is also closed.

3.msg.target.dispatchMessage() is handed over to the Handler to actually process the message. Since the current execution thread is the main thread, the callback executed in dispatchMessage is also in the main thread.

4. We can see that there is logging in the code for input printing, which is very helpful for our performance monitoring, which we will explain in Chapter 7.

5.msg.recycleUnChecked marks that msg has been used and enters a reusable state.

4.4 dispatchMessage executes message

There are two ways to call back,

Method 1 directly executes the callBack in the Message;

The second way is to customize the Handler and override its handleMessage method.

Here we can clearly see that the priority of callBack will be higher. This is technical point 3: which one of CallBack and handleMessage will be executed first

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

In handleCallBack, it is directly through

message.callback.run();

Execute the runnable task of Message. PS: run() is a method defined in the Runnable interface

Five, MessageQueue.next() to get the message

Look at the code first, which is divided into the following links.

Message next() {
        ...
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);//4.1 nativePollOnece

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                ...屏障消息相关代码先忽略
                if (msg != null) {//4.2 寻找可用message
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // 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();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

               ...idelHandler相关代码先忽略
        }
    }

5.1 nativePollOnce method

This is a native method, blocking. The CPU sleep we mentioned above is also achieved through the mechanism of nativePollOnce. When this method is called, the CPU resources will be released until it is awakened by the outside world. The underlying implementation principle corresponds to the epoll mechanism of linux, and we will not analyze it in detail in this article. We just need to know the function of its method.

nativePollOnce has two parameters, ptr and timeoutMillis

ptr can be understood as a unique value of a native tag, used to identify the bound thread.

timeoutMillis is the timeout period. Mainly divided into three kinds -1, 0, > 0.

=-1 is in a state of infinite blocking unless awakened by the outside world.

When =0 , it will not block and will be executed immediately.

>0 will block the corresponding time, and then release the blocking state.

PS: The native implementation of nativePollOnce is actually somewhat similar to the Android layer, and also has a blocking circular queue. The underlying implementation mechanism is epoll. Since the native layer is not the core of this article, I will not expand the introduction here. Interested students can leave a message.

5.2 Traverse the linked list to find available messages

1. Because the linked list is inserted according to the execution time, the first executed Message must be at the top of the linked list;

2. First get the current system non-sleep state time;

3. First try to get the head of the linked list, then there is no data in the linked list. Then assign nextPollTimeoutMillis to -1, and in the next cycle, it will enter an infinite blocking state and be woken up directly. This corresponds to the native wake-up mechanism mentioned in Chapter 2.5 .

4. If the head node is not empty, compare its execution time with the current time;

5. If its execution time is less than the current time, calculate the difference nextPollTimeoutMillis. And will jump out of this Message selection process. And in the next loop, nativePollOnce will use this value to sleep for the corresponding time. It is guaranteed that when the sleep time arrives, it is just the execution time of the head node.

6. If its execution time is greater than the current time, it indicates that the node can be executed. Change the head node to the next level node. And mark the current Message has been used by Message.markInUse.

7. Return to the message object found in the previous step

5. Asynchronous message/barrier message

Technical point 4: Implementation mechanism of barrier asynchronous messages

1 The barrier message is actually a Message message with an empty target.

2 Asynchronous messages must be used with barrier messages.

3 If the head node is a barrier message, it will search for asynchronous messages from front to back in the message linked list, and jump out of the loop when an asynchronous message is found.

4 And remove the current asynchronous node from the linked list, and associate the two nodes before and after the asynchronous node.

5 It should be noted here that the head node is still the barrier message and has not been removed. So normal messages are still not executed.

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());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;//把异步消息的next赋值给前面那个节点的next
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

6. IdleHandler

Technical point 5: IdelHandler execution timing

6.1 Introduction to IdleHandler

As the name suggests, it is a free time task. When there is currently no Message to be executed, the execution of IdelHandler will be triggered. We can generally put tasks that need to be executed on the main thread but with low priority in the IdelHandler for execution, such as preloading the next page, loading the second screen, etc. Wait.

6.2 When does IdleHandler trigger

Looking at the code, I can know that when the Message is obtained, if the Message is not obtained, it will enter the process of executing the IdleHandler.

Message next() {
       ...代码省略
        for (;;) {
            
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;  //如果取到了消息则返回
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                ...
                //如果没有取到消息,则会执行下面的逻辑
                // 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;
        }
    }

6.3 IdleHandler execution process

There can be multiple IdleHandlers, which are stored in the MeesageQueue by mIdelHandlers and are converted to arrays and executed in sequence each time they are executed.

 pendingIdleHandlerCount = mIdleHandlers.size(); 
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
 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);
                    }
                }
            }

It should be noted here that the callback of IdleHandler will only be executed once and will be removed after the execution is complete.

Seven, Message execution monitoring

Technical point 6 : How to monitor the main thread stuck problem

7.1 Triggering principle

In chapter 3.3, we mentioned the logging object. Let's look back at the code, and here we can clearly see that logging will be called before and after the message is executed. The time between these two prints can be considered as the execution time of the callback in the Message.

private static boolean loopOnce(final Looper me,
            final long ident, final int thresholdOverride) {
        ...省略代码
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " "
                    + msg.callback + ": " + msg.what);
        }
        ...省略代码
        try {
            msg.target.dispatchMessage(msg);
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } catch (Exception exception) {
        ...省略代码
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

       
    }

7.2 How to use

Let's take a look at the Printer class corresponding to logging, which is actually an interface.

public interface Printer {
    /**
     * Write a line of text to the output.  There is no need to terminate
     * the given string with a newline.
     */
    void println(String x);
}

And we found that logging allows us to actively set it up. When used, the Looper.mLogging object is taken.

The setMessageLogging method just sets mLogging.

 public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }

So we only need to create a Printer object and register it in Looper, then through the time of two callbacks, we can judge which Meesage execution timed out. The following code can monitor the scenario where all Message messages of the main thread are executed for more than 100 milliseconds.

//声明Printer对象
private Printer printer = new Printer() {
        @Override
        public void println(String it) {
            long currentTimeMillis = System.currentTimeMillis();
            //其实这里应该是一一对应判断的,但是由于是运行主线程中,所以Dispatching之后一定是Finished,依次执行
            if (it.contains("Dispatching")) {
                lastFrameTime = currentTimeMillis;
                return;
            }
            if (it.contains("Finished")) {
                long useTime = currentTimeMillis - lastFrameTime;
                //记录时间
                if (useTime > 100) {
                    //todo 要判断哪里耗时操作导致的
                    Log.i(TAG, "执行超过100毫秒");
                }
            }
        }
    };

//然后注册
 Looper mainLooper = Looper.getMainLooper();
 mainLooper.setMessageLogging(printer);

7.3 Application Scenarios

Continuing to expand, it is definitely not enough if we just know whether the main thread is stuck or not. We must also wonder where the card is? This is also what BlockCanary wants to solve. However, we also have a simple implementation here, one class can complete performance monitoring.

1. We can start a child thread to continuously capture the stack state of the main thread every specified time (such as 20 milliseconds).

2. When the println callback notifies the Message's callback execution to start, we store each captured stack in the Map.

3. When the println callback notification ends, we judge the execution time. If the timeout is exceeded, all the stack structures in the Map will be printed. If there are two identical stack structures in the Map, it means that the method corresponding to this stack has been executed for at least 20 milliseconds (up to 40 milliseconds). If there are 3, it has executed for at least 40ms, and so on.

4. So by the number of times the same stack is printed, we know where the jam is caused. I often use it in the development and debugging phase, and it is very easy to use.

The complete code of the small performance monitoring class is attached:

package com.common.monitor;

import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;
import android.util.Printer;

import com.common.monitor.monitor.BaseMonitor;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class ANRMonitor{

    final static String TAG = "anr";

    public static void init(Context context) {
        if (true){//开关
            return;
        }
        ANRMonitor anrMonitor = new ANRMonitor();
        anrMonitor.start(context);
        Log.i(TAG, "ANRMonitor init");
    }

    private void start(Context context) {
        Looper mainLooper = Looper.getMainLooper();
        mainLooper.setMessageLogging(printer);
        HandlerThread handlerThread = new HandlerThread(ANRMonitor.class.getSimpleName());
        handlerThread.start();
        //时间较长,则记录堆栈
        threadHandler = new Handler(handlerThread.getLooper());
        mCurrentThread = Thread.currentThread();
    }

    private long lastFrameTime = 0L;
    private Handler threadHandler;
    private long mSampleInterval = 40;
    private Thread mCurrentThread;//主线程
    private final Map<String, String> mStackMap = new HashMap<>();

    private Printer printer = new Printer() {
        @Override
        public void println(String it) {
            long currentTimeMillis = System.currentTimeMillis();
            //其实这里应该是一一对应判断的,但是由于是运行主线程中,所以Dispatching之后一定是Finished,依次执行
            if (it.contains("Dispatching")) {
                lastFrameTime = currentTimeMillis;
                //开始进行记录
                threadHandler.postDelayed(mRunnable, mSampleInterval);
                synchronized (mStackMap) {
                    mStackMap.clear();
                }
                return;
            }
            if (it.contains("Finished")) {
                long useTime = currentTimeMillis - lastFrameTime;
                //记录时间
                if (useTime > 20) {
                    //todo 要判断哪里耗时操作导致的
                    Log.i(TAG, "ANR:" + it + ", useTime:" + useTime);
                    //大于100毫秒,则打印出来卡顿日志
                    if (useTime > 100) {
                        synchronized (mStackMap) {
                            Log.i(TAG, "mStackMap.size:" + mStackMap.size());
                            for (String key : mStackMap.keySet()) {
                                Log.i(TAG, "key:" + key + ",state:" + mStackMap.get(key));
                            }
                            mStackMap.clear();
                        }
                    }
                }
                threadHandler.removeCallbacks(mRunnable);
            }
        }
    };


    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            doSample();
            threadHandler
                    .postDelayed(mRunnable, mSampleInterval);
        }
    };

    protected void doSample() {
        StringBuilder stringBuilder = new StringBuilder();

        for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
            stringBuilder
                    .append(stackTraceElement.toString())
                    .append("\n");
        }
        synchronized (mStackMap) {
            mStackMap.put(mStackMap.size() + "", stringBuilder.toString());
        }
    }

}

Guess you like

Origin blog.csdn.net/AA5279AA/article/details/123187510