HandlerThread source code analysis

HandlerThread is familiar to everyone. From the name, it is a Thread with Handler message loop mechanism. It has more message loop mechanism than ordinary threads. It can be said that it is a combination of Handler+Thread. From the source code, it is also designed in this way. Generally, if it is necessary to interact between sub threads and main threads, it can be designed with HandlerThread, This is more convenient and easier to manage than a simple Thread, because we all know that the life cycle of a Thread is uncontrollable in some cases, such as direct new Thread().start(), which is not recommended in the project. In fact, HandlerThread is used in many places in the Android source code, Next, I will analyze HandlerThread and some other related problems involved.

Simple analysis of the source code of HandlerThread

    int mPriority;
    int mTid = -1;
    Looper mLooper;
    private @Nullable Handler mHandler;

    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }

First, the field has priority. You can see that the priority in HandlerThread is the default. Of course, you can also modify it. Just call the constructor with two parameters. There is a String type in the constructor that represents the name of HandlerThread. In general, you can take the name of the thread.

 /**
     * Call back method that can be explicitly overridden if needed to execute some
     * setup before Looper loops.
     */
    protected void onLooperPrepared() {
    }

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }

There is a method onLooperPrepared(). In practice, we can rewrite this method to do some initialization operations. This run() is the focus. You can see that Looper has been initialized and started to receive messages circularly, and all waiting threads have been awakened. Because the run method will not be started until after start, when using HandlertThread, After initializing the instance, you must call the start method to start the message loop.

 public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

This method is to get the current looper. You can see that if you don't get it, you will wait until you get it. As mentioned earlier, when you get it, you wake up all threads. It seems that this is the application of thread wait wake mechanism.

public Handler getThreadHandler() {
        if (mHandler == null) {
            mHandler = new Handler(getLooper());
        }
        return mHandler;
    }

This is the Handler of the Looper thread bound by the HandlerThread

public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit();
            return true;
        }
        return false;
    }
    
    
 public boolean quitSafely() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quitSafely();
            return true;
        }
        return false;
    }

You can see that these two methods exit the Looper loop of the thread. What is the difference between these two methods? In fact, they call the quit() method of MessageQueue. The source code is as follows:

void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }

        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;

            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }

            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
}

We can see that when we call the quit method, we actually execute the removeAllMessagesLocked method in the MessageQueue. This method is used to empty all messages in the MessageQueue message pool, whether delayed messages (delayed messages refer to messages sent by sendMessageDelayed or postDelayed methods that require delayed execution, as long as the messages that are not executed immediately are delayed messages) or non delayed messages.

When using the quitsafe method, the removeAllFutureMessagesLocked method in the MessageQueue is actually executed. As can be seen from the name, this method will only empty all delayed messages in the MessageQueue message pool and send all non delayed messages in the message pool to the Handler for processing. Compared with the quit method, the safety of quitsafe is that it will be sent before emptying messages All non deferred messages, in a word, are messages that need to be executed in the future.

The two methods have a common feature: the Looper will no longer receive new messages, and the message cycle ends. At this time, the messages sent through the Handler will not be put into the message queue because the message queue has exited. When applying these two methods, it should be noted that the quit method has existed since API 1, which is relatively early, and the quitsafe method was not added until API 18 Come in

This concludes the source code analysis. Let's take a practical and simple example:

  private static class UserHandlerThread extends HandlerThread implements Handler.Callback {
        private Handler mHandler;
        public UserHandlerThread(String name) {
            super(name);
            start();
            mHandler = new Handler(getLooper(), this);
        }

        @Override
        protected void onLooperPrepared() {
            super.onLooperPrepared();
        }

        @Override
        public boolean handleMessage(Message msg) {
            if (msg.what==0x123){
                Log.d("[app]","Got the message");
            }
            return false;
        }

        public Handler getHandler() {
            return mHandler;
        }
    }
    
    
    private UserHandlerThread handlerThread;
    handlerThread = new UserHandlerThread("handlerThread");
    handlerThread.getHandler().sendEmptyMessage(0x123);
    

OK, everyone knows the result. Generally, you need to exit the loop in OnDestroy method, such as the following code:

@Override
    protected void onDestroy() {
        super.onDestroy();
        if (handlerThread != null) {
            handlerThread.getHandler().removeCallbacksAndMessages(null);
            handlerThread.quit();
            handlerThread = null;
        }
    }
    //Looper.getMainLooper().quit();

Let's ignore the comment first. OK, let's analyze three related problems:

● since the child thread can exit the message loop queue, can the UI thread, that is, the main thread, also exit the message loop in the onDestroy method, that is, cancel the comments of the code just now

Let's start with the answer: you can't exit the message queue of the main thread, or you will throw a Main thread not allowed to quit. Is it familiar? Yes, it has been posted in the above code. Why? The MessageQueue has a field: mQuitAllowed

 // True if the message queue can be quit.
    private final boolean mQuitAllowed;

This field represents whether the message queue can be exited. What is the field of the main thread? false means that the message queue of the current message cannot be exited. Let's look at the message initialization code of the main thread: Looper.prepareMainLooper();

public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
    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));
    }
    
     private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

You can see that in the prepare parameter, the main thread passes in false, which is assigned in the Looper constructor, that is, the main thread is false, so this determines that the main thread cannot exit the message queue. Why can the child thread exit the message queue? Because the child thread passes in true, the code is as follows:

Looper.prepare();
 public static void prepare() {
        prepare(true);
    }

See, the value passed in by the child thread is true, which means that the child thread can exit the message queue. In fact, it is the work of the FrameWork layer to really exit the message queue of the main thread. The third problem will be mentioned below.

● which thread does the message sent from the Handler execute on?

This article does not intend to analyze the source code of the Handler in a long way, but only briefly discuss the problems related to the Handler thread. In fact, for the Handler, if I can understand the following figure and then analyze the source code myself, I think it will be deeper

As we know, there is a parameter in the constructor of handler, which is to set the current Looper. The code is as follows:

 /**
     * Use the provided {@link Looper} instead of the default one.
     *
     * @param looper The looper, must not be null.
     */
    public Handler(Looper looper) {
        this(looper, null, false);
    }
     public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
    mQueue = mLooper.mQueue;

It can be seen that the incoming Looper will eventually be assigned to the mloop field. If this constructor is not called, the mloop will be obtained by the current thread, and the message queue MessageQueue is obtained by the Looper. In other words, which Looper the Handler's message circulates, it means that all messages will be stored and processed by the Looper's message queue, and a thread can only be bound Set a Looper, so a Handler can only bind one Looper, which means that if the Handler binds the Looper of which thread, then all messages will be executed on which thread, which is consistent with the figure above. Of course, some students say that I open a thread in the Handler, but the Handler operation itself must be on the thread of the corresponding Looper, that is If the Handler is bound to the Looper of the main thread, all messages will be processed in the main thread. If the Handler is bound to a child thread, all messages will be processed in the child thread.

● what is the real meaning of "exiting" an application on Android?

For this problem, some students may say that if I kill the process and the application hangs, it is equivalent to quitting. In fact, this statement is incorrect, because killing the process here is not only the program itself, but also the memory space of the program. As we said earlier, the sub thread can exit the message queue, which means that the sub thread can no longer receive any messages. This is the meaning of the sub thread exit. In fact, the main thread is the same, This process is invisible to our developers. If the main thread exits the message queue, it means that the main thread cannot receive any messages. The following code is in ActivityThread.java:

 public final void scheduleExit() {
            sendMessage(H.EXIT_APPLICATION, null);
        }
 case EXIT_APPLICATION:
                    if (mInitialApplication != null) {
                        mInitialApplication.onTerminate();
                    }
                    Looper.myLooper().quit();
                    break;

Seeing this, I think all students familiar with IPC process should know that, yes, scheduleExit() is not called by the local process, but by the server process ActivityAManagerService service. This is why I say that exiting the main thread is called by the FrameWork. There are two places in AMS that call the exit code, It is called when binding the local process and memory recycling. The following code (in ActivityManagerService.Java):

stay APP Local process bound to AMS Called when the process is running
 private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid) {

        // Find the application record that is being attached...  either via
        // the pid if we are running in multiple processes, or just pull the
        // next app record if we are emulating process with anonymous threads.
        ProcessRecord app;
        if (pid != MY_PID && pid >= 0) {
            synchronized (mPidsSelfLocked) {
                app = mPidsSelfLocked.get(pid);
            }
        } else {
            app = null;
        }

        if (app == null) {
            Slog.w(TAG, "No pending application record for pid " + pid
                    + " (IApplicationThread " + thread + "); dropping process");
            EventLog.writeEvent(EventLogTags.AM_DROP_PROCESS, pid);
            if (pid > 0 && pid != MY_PID) {
                Process.killProcessQuiet(pid);
                //TODO: killProcessGroup(app.info.uid, pid);
            } else {
                try {
                //It's also called here
                    thread.scheduleExit();
                } catch (Exception e) {
                    // Ignore exceptions.
                }
            }
            return false;
        }
        //Omit some unnecessary code
    }


//i called when clearing memory
final void trimApplications() {
        synchronized (this) {
            int i;

            // First remove any unused application processes whose package
            // has been removed.
            for (i=mRemovedProcesses.size()-1; i>=0; i--) {
                final ProcessRecord app = mRemovedProcesses.get(i);
                if (app.activities.size() == 0
                        && app.curReceiver == null && app.services.size() == 0) {
                    Slog.i(
                        TAG, "Exiting empty application process "
                        + app.toShortString() + " ("
                        + (app.thread != null ? app.thread.asBinder() : null)
                        + ")\n");
                    if (app.pid > 0 && app.pid != MY_PID) {
                        //Kill process
                        app.kill("empty", false);
                    } else {
                        try {
                        //The exit main thread message queue code is called here
                            app.thread.scheduleExit();
                        } catch (Exception e) {
                            // Ignore exceptions.
                        }
                    }
                    cleanUpApplicationRecordLocked(app, false, true, -1, false /*replacingPid*/);
                    mRemovedProcesses.remove(i);

                    if (app.persistent) {
                        addAppLocked(app.info, false, null /* ABI override */);
                    }
                }
            }

            // Now update the oom adj for all processes.
            updateOomAdjLocked();
        }
    }

You see, when the App binds the process for the first time, if the error app==null occurs, it will call to exit the main thread message queue. The other is called when clearing the memory. These two processes are invisible to our developers. We just know that there is nothing magical in front of the source code.

Thank you for reading. If there are any mistakes, please point them out. Thank you.

Relevant video recommendations:
[Android interview collection] handler source code analysis of 5-FrameWork source code (I) - 01
[Android interview collection] handler source code analysis of 6-FrameWork source code (I) - 02

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

Keywords: Android

Added by akeane on Wed, 17 Nov 2021 03:44:09 +0200