Read this article with questions in mind and you’ll get more out of it.

  • Why use handler? In what scenario? Can’t I?
  • Is it true that the main thread can’t update the UI?
  • How is Handler used?
  • What are the problems with Headler?
  • What is IdleHandler? In what scenario?
  • Have you ever heard of synchronous message barriers? What’s the use?
  • What is a HandlerThread? What is his implementation principle? What’s the use?
  • What is an IntentService? What’s the use?

What is Handler?

A: The message communication mechanism in Android is used for the communication between the child thread and the main thread. It realizes a non-blocking message passing mechanism. The UI update information in the child thread is sent to the main thread (UI thread), so as to complete the UI update operation. In Android, the main thread cannot perform time-consuming operations. All time-consuming operations need to be carried out in the child thread. If the UI needs to be updated after the time-consuming operation, the information needs to be passed to the main thread to update the UI.

Is it true that child threads cannot update the UI?

Let’s look at an example:

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread(new Runnable() { @Override public void run() { Looper.prepare(); AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); Builder.setmessage (" demo UI update on child thread "); AlertDialog alertDialog = builder.create(); alertDialog.show(); Looper.loop(); } }).start(); }Copy the code

After the operation:

As you can see, the dialog box pops up normally, and we usually update the UI in the child thread with an error message like this:

Error: Only the thread that created the View can update the View. This error is reported because the View is initialized in the mainThread. So the child thread can update the UI, but only the View created by the child thread.

Why does the Android mechanism prevent child threads from being able to update the main thread? This is because multiple threads are updating the same UI control at the same time, which is prone to uncontrollable errors, namely thread-safety issues. So how do you solve this thread safety problem? The simplest way to do this is to lock, not just one, but every layer (user code →GUI top layer →GUI bottom layer…) However, it also means more time consuming, which leads to lower EFFICIENCY of UI update and interface lag.

Let’s look at another scenario where the UI is updated on a child thread

public class MainActivity extends AppCompatActivity { private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = findViewById(R.id.tv); new Thread(new Runnable() { @Override public void run() { textView.setText(Thread.currentThread().getName()); } }).start(); }}Copy the code

This code updates the UI directly in the child thread without error:

The thread that created the view has the right to modify the view. The thread that created the view has the right to modify the view. The thread that created the view has the right to modify the view.

  1. ViewRootImponCreate()It was not created at the time of the callonResume()namelyActivityThread.handleResumeActivity()* Created after execution;
  2. callView.requestLayout(), and finally switched toViewRootImpl.requestLayout()To the * *checkThread()* * error;

So we suspect that the child thread can modify the UI because of the method execution order. Let’s verify:

public class MainActivity extends AppCompatActivity { private TextView textView; private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = findViewById(R.id.tv); Log.d(TAG, "onCreate: "); New Thread(new Runnable() {@override public void run() {log.d (TAG, "run: modify UI in child Thread "); textView.setText(Thread.currentThread().getName()); } }).start(); } @Override protected void onResume() { super.onResume(); Log.d(TAG, "onResume: "); }}Copy the code

The running results are as follows:

The 2022-03-13 11:31:03. 370, 5964-5964 / com. Firstcode. Myapplication D/MainActivity: onCreate: The 2022-03-13 11:31:03. 373, 5964-5998 / com. Firstcode. Myapplication D/MainActivity: run: In the child thread to modify the UI 2022-03-13 11:31:03. 435. 5964-5964 / com firstcode. Myapplication D/MainActivity: onResume:Copy the code

Add hibernation to the child thread code block to simulate time-consuming operations:

new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(300); Log.d(TAG, "run: modify UI in child thread "); } catch (InterruptedException e) { e.printStackTrace(); } textView.setText(Thread.currentThread().getName()); } }).start();Copy the code

Program crash, error log as follows:

  android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7753)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1225)
        at android.view.View.requestLayout(View.java:23093)
        at android.view.View.requestLayout(View.java:23093)
        at android.view.View.requestLayout(View.java:23093)
        at android.view.View.requestLayout(View.java:23093)
        at android.view.View.requestLayout(View.java:23093)
        at android.view.View.requestLayout(View.java:23093)
        at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3146)
Copy the code

The execution logs are as follows:

The 2022-03-13 11:39:09. 674, 6306-6306 / com. Firstcode. Myapplication D/MainActivity: onCreate: The 2022-03-13 11:39:09. 684, 6306-6306 / com. Firstcode. Myapplication D/MainActivity: onResume: The 2022-03-13 11:39:09. 979, 6306-6338 / com. Firstcode. Myapplication D/MainActivity: run: modify the UI in the child threadCopy the code

So the child thread here can update the UI because of the order in which the methods are executed, update the UI first and then perform the check logic, which is a coincidence, but it’s not really possible to update the UI in the child thread.

How is Handler used?

3.1 In the Default Thread (used in the main thread)

Android.os. Handler Handler = new Handler(){@override public void handleMessage(final Message MSG){ Message = message.obtain (); handler.sendMessage(message); // Handler. Post (new Runnable() {@override public void run() {}});Copy the code

The way of use is relatively simple:

  1. Create Handler object;
  2. Just send the message

3.2 Sending Messages between two threads (the child thread sends messages to the main thread)

public class MainActivity extends AppCompatActivity { private TextView textView; private static final String TAG = "MainActivity"; Android.os. Handler Handler = new Handler() {@override public void handleMessage(Message MSG) { "handleMessage: " + msg.what); }}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread() { @Override public void run() { super.run(); Try {// The child thread sends Message message1 = message.obtain (); message1.what = 1; handler.sendMessage(message1); } catch (Exception e) { e.printStackTrace(); } } }.start(); }}Copy the code
The 2022-03-13 12:07:29. 511, 6843-6843 / com. Firstcode. Myapplication D/MainActivity: handleMessage: 1Copy the code

3.3 Two Threads Send Messages to each other (main thread sends messages to child threads)

Two threads send messages to each other in the same way, just remember: A sends A message to B’s handler, A sends A message to B’s handler, and A calls handler.sendMessage().

public class MainActivity extends AppCompatActivity { private TextView textView; private static final String TAG = "MainActivity"; android.os.Handler handler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread() { @Override public void run() { super.run(); Looper.prepare(); Handler = new handler () {@override public void handleMessage(Message MSG) {log. d(TAG, "handleMessage: " + msg.what); }}; Looper.loop(); } }.start(); try { Thread.sleep(200); } catch (InterruptedException e) {e.printStackTrace(); } Message message1 = Message.obtain(); message1.what = 2; handler.sendMessage(message1); }}Copy the code
The 2022-03-13 12:13:48. 756, 7305-7337 / com. Firstcode. Myapplication D/MainActivity: handleMessage: 2Copy the code

Sending a message to a child thread is slightly different in that it calls Looper’s method first.

We recommend calling the message.obtain () function to obtain an instance of Message. Why? Click on source code:

From the source code, you can see the logic of obtain() : lock → determine whether the Message pool is empty

  • (1) Select a Message object, set it to 0 with the flag, pool capacity -1, and return this object.
  • If the value is null, a new Message object is returned.

Message is actually a single linked list with no head nodes.

The logic for getting the message pool above:

Locating the following code also tells you that the pool has a capacity of 50

Then the question arises, when is the Message added to the pool?

A: After the Message is distributed by Looper, the recycleUnchecked() function is called to recycle unused Message objects.

Set the flag to **FLAG_IN_USE** to indicate that the message is being used, reset the related properties, lock, determine whether the message pool is full or not, single-linked header inserts the message into the header.

Parse the underlying principle of Handler

Finally came to a little bit of the technical content of the link, before looking at the source code to understand the principle, first say a few involved in the class.

4.1 Classes involved

  • Handler: 1. Send messages to MessageQueue. 2. Process the message
  • Message: Message carrier
  • Looper: Internally maintains MessageQueue, constantly fetching messages from MQ on a rotating basis and sending them to handler for processing
  • MessageQueue: MessageQueue, in Looper, responsible for storing messages
  • ThreadLocal: A thread has a ThreadLocal, stores Looper, binds thread to Looper, and makes each thread’s message queue thread-safe

4.2 Overall Process

4.3 stars. Prepare () :

public static void prepare() { prepare(true); } 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)); }Copy the code

Looper uses ThreadLocal to bind to the current thread

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
Copy the code

In this process, several things are done

  1. Create “Looper”;
  2. After Looper is created, a “MessageQueue” object for managing messages is created in the constructor of Looper
  3. Bind to the current thread via ThreadLocal

Prepare (true); prepare(true); prepare(true); prepare(true);


public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}
Copy the code

Trace MessageQueue source code:

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); }}Copy the code

As you can see, if it’s a child thread we can stop the message queue, but we don’t have this permission for the main thread.

4.4 stars. The loop ()

After the previous step, Looper and MessageQueue objects are created and looper.loop () is called to enable polling.

public static void loop() { final Looper me = myLooper(); If (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; // Get message queue... for (;;) Message MSG = queue.next(); // might block msg.target.dispatchMessage(msg); // Send the message to handler}Copy the code

I mainly did several things:

  • Get the current thread Looper instance;
  • Get message queue
  • Start an endless loop to retrieve messages from the queue
  • The message is distributed to handler for processing

MyLooper () function:

@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}
Copy the code

ThreadLocal → Thread-local variables → tools provided by the JDK to address thread-safety. Purpose: Provide a separate copy of variables for each thread → to solve the problem of concurrent access conflicts

ThreadLocal.class:

/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // Get ThreadLocalMap if (map! = null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e ! = null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } private Entry getEntry(ThreadLocal<? > key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e ! = null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }Copy the code

Each Thread maintains a ThreadLocalMap whose key is ThreadLocal and value is the set value. When getting, threads take values from their own variables, so there is no thread-safety issue.

Summary:

  1. Looper.prepare() saves the current Looper to ThreadLocal.
  2. Each thread has a ThreadLocalMap where the key is the current thread and the value is the Looper saved above. So passing looper.mylooper will get the looper object of the current thread and be unique

Main thread and child thread Looper object instances are isolated from each other!!

In addition, the thread key also ensures that each thread has only one Looper. Creating a Looper object also creates a MessageQueue object, so it indirectly guarantees that each thread can have at most one MessageQueue.

With that in mind, one question becomes clear:

Why can’t a child thread directly new Handler() when the main thread can?

Since the primary thread and its child threads do not share the same Looper instance, the Looper of the main thread is initialized from prepareMainLooper() at startup, and the child thread also needs to call looper.prepare () and looper.loop () to enable polling. Otherwise an error will be reported, do not believe, you can try:

It just broke down

Plus try?

No error was reported and the program ran normally.

Handler is used by the child thread to communicate with the main thread, so try sending a message to the child thread’s Handler in the main thread.

Run, direct error:

Cause: Multithreading concurrency problem, when the main thread executes to sendEnptyMessage, the child thread Handler has not been created. A simple workaround is that the main thread delays sending messages to child threads, as shown in the following code example:

The running results are as follows:

Yes, but Android already packages a lightweight asynchronous class called HandlerThread

4.5 What happens when we send a message with Handler?

Handler can send messages via sendMessage() and post(), both of which are actually called by sendMessageDelayed() :

Handler.java:

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

public final boolean post(@NonNull Runnable r) {
   return  sendMessageDelayed(getPostMessage(r), 0);
}

Copy the code

Second parameter: current system time + delay time, this will affect the “scheduling order”

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);
}
Copy the code

Get MessageQueue queue in the current thread Looper, declare null, null print exception, otherwise return 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);
}
Copy the code

If the Handler constructor does not pass this parameter, the default is false: there is a “synchronization barrier” involved, etc., as in MessageQueue -> enqueueMessage

MessageQueue.java

boolean enqueueMessage(Message msg, long when) { if (msg.target == null) { throw new IllegalArgumentException("Message must have a target."); } if (msg.isInUse()) { throw new IllegalStateException(msg + " This message is already in use."); } synchronized (this) { 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; }Copy the code

This is all very simple if you understand singly linked lists in data structures.

4.6 How does Looper process queued messages?

If MessageQueue contains a Message, then it should be handled by Looper

// Looper.loop() final Looper me = myLooper(); Final MessageQueue queue = me.mqueue; // Get the message queue for (;;) {// Message MSG = queue.next(); / / remove the queue of messages in the MSG. Target. DispatchMessage (MSG); // Distribute the message to Handler}Copy the code

Queue.next () retrieves the message from the queue and locates it to MessageQueue -> next:

NextPollTimeoutMillis determines whether or not to block, and when to block.

= 0, no blocking, return immediately, Looper first processing message, one message is finished;

Greater than 0, the maximum blocking wait time, during which a new message may return immediately (execute immediately);

If it’s -1, it’s always blocked when there’s no message;

This is blocked by the **epoll mechanism ** of Linux, because events on the native side need to be processed.

When there is no message, it blocks and goes to sleep to release CPU resources. When there is a message, it wakes up again.

4.7 How are Messages sent to Handlers processed?

Picked out by MessageQueue queue. Next (), call MSG. Target. DispatchMessage (MSG) the message distributed to the corresponding Handler, with that Handler – > dispatchMessage

What is IdleHandler?

Add idleHandler to it, then arrange it

There is a static interface IdleHanlder in the MessageQueue class

This interface is called back when a thread is about to block, waiting for more messages. Simply put: a callback when there is no Message in a MessageQueue to process; What it does: After the UI thread has finished processing all View transactions, it calls back some additional operations without blocking the main process.

There is only one queueIdle() function in the interface, where additional operations performed when a thread is blocked can be written. If the return value is true, the IdleHandler will be retained after the execution of this method, otherwise deleted.

The usage method is also very simple, the code example is as follows:

The following output is displayed:

MessageQueue defines a list and array of idleHandlers

Defines functions to add and remove IdleHandler:

The mIdleHandlers list is used in the next() function:

Six, some other issues

1.Looper is an infinite loop in the main thread, why not ANR?

Looper gets message queue messages from **queue.next()**. When the queue is empty, it will block.

The main thread is blocked here, and the good thing is: the main function cannot exit, and the APP does not end as soon as it starts!

You may be asking: How do you respond to user actions and callback Activity life-cycle methods when the main thread is blocked?

Binder threads: ApplicationThread and ActivityManagerProxy Are used to communicate with system processes and receive notifications from them.

  • Binder notifies ApplicationThread across processes when the system is notified of user operations.
  • It uses a Handler mechanism to insert a message into ActivityThread’s MessageQueue to wake up the main thread.
  • Queue.next () gets the message and dispatchMessage completes the event distribution;

Tips: The inner class H in ActivityThread has an implementation

There is no ANR in a dispatchMessage. If you perform some time-consuming operation here, the message is not finished, and then you receive too many messages, it will cause ANR exception!!

2. The Handler leak causes and correct writing method

If you initialize a Handler object directly in your Activity, you will get the following error:

The reason is:

In Java, a non-static inner class holds an implicit reference to an external class, which can cause the external class to fail to be GC; The Handler here, for example, is a non-static inner class that holds a reference to the Activity so that it cannot be released properly.

With static inner classes alone, the Handler cannot call non-static methods in the Activity, so it adds a “weak reference” to the external Activity.

A code example is as follows:

Private Static Class MyHandler extends Handler {private final WeakReference<MainActivity> content; private Static Class MyHandler extends Handler {private final WeakReference<MainActivity> content; private MyHandler(MainActivity content) { this.content = new WeakReference<MainActivity>(content); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); MainActivity activity= content.get(); if (activity ! = null) { switch (msg.what) { case 0: { activity.notifyUI(); } } } } }Copy the code

In addition, there are other situations that can cause memory leaks: delayed messages, which can be called in the Activity’s onDestroy() function while the Activity is still being processed: Handler. RemoveCallbacksAndMessages (null) to remove the Message/Runnable.

3. Synchronization barrier mechanism

MessageQueue enqueueMessage() inserts messages into the queue in ascending order of time stamps, while Looper distributes messages one at a time in ascending order. One goes on to the next.

There is an urgent Message that needs to be processed first. You might say ** just sendMessage()** no, don’t wait to execute it immediately, which seems reasonable, but there might be a case like this:

When a Message is sent to a Handler and a time-consuming operation is performed, the next batch of messages that are supposed to be executed are waiting, and you have to sendMessage() behind them and wait for them to finish.

Isn’t it? A synchronization barrier mechanism has been added to the Handler to enable asynchronous message execution first.

Adding an asynchronous message is simple:

  • The Handler constructor passes the async argument to true, and all messages added with this Handler are asynchronous.
  • Call setAsynchronous(true) when creating a Message object.

In general: Synchronous and asynchronous messages are not much different, but only until the synchronization barrier is enabled. MessageQueue’s postSyncBarrier function can be used to enable synchronization barriers:

Insert the synchronization barrier Message (target null) to the appropriate position in the Message queue. Then, when the MessageQueue is executed to next() :

If a Message with a target of NULL is encountered, it is a synchronization barrier and iterates to find an asynchronous Message and processes it. Until the synchronization barrier is removed, only asynchronous messages are processed, and when all asynchronous messages are processed, they are blocked. To resume processing synchronization messages, call removeSyncBarrier() to remove the synchronization barrier:

In API 28, postSyncBarrier() is marked hide, but can still be found in the system source code. For example, scheduleTraversals in ViewRootImpl are used for faster response to UI refresh events: