Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”


This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money

preface

Generally, there are many threads in App, and it is inevitable to communicate with each other. In our usual development, the most common thread communication is Handler, such as child thread for data processing, UI update in the main thread.

Of course, there are several other ways to communicate between threads besides Handler: pips, shared memory, files, databases, etc.

Let’s focus on Handler and how it works


Android Advanced: Fully embrace the Activity Results API instead of onActivityResult

Android source code advanced in-depth understanding of SharedPreference principle mechanism

Study should “combine work and rest”

Android advanced desktop tasks and scheduling services and abandoned AlarmManager to fully embrace WorkManager

Android transition animation depth analysis

Java advanced in-depth understanding of load balancing algorithm implementation principle of 5

Advanced Coil for Android – Full explanation of kotlin’s photo gallery

History recommendation, mutual encouragement

Happy National Day: study and progress together

A, Looper dead loop detailed explanation

1. Why does an endless loop not cause an application to freeze ANR

  • Threads do not have Looper by default. If you want to use a Handler, you must create a Looper for the thread.
  • The main thread we often refer to, also known as the UI thread, is an ActivityThread. An ActivityThread initializes a Looper when it is created, which is why you can use a Handler in the main thread by default.
  • ActivityThread, and in the main method we see that the main thread also maintains a message loop in Looper mode

public static void main(String[] args) {

Looper.prepareMainLooper();// Create Looper and MessageQueue objects to handle messages for the main thread

ActivityThread thread = new ActivityThread();

thread.attach(false);// Create a new thread.

if (sMainThreadHandler == null) {

sMainThreadHandler = thread.getHandler();

}

Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

Looper.loop();

// The application crashes or exits if you execute the following method...

throw new RuntimeException("Main thread loop unexpectedly exited");

}
Copy the code

Does this loop cause the application to freeze, and even if it doesn’t, does it slowly consume more and more resources?

① For a thread is a piece of executable code, when the executable code execution is completed, the thread life cycle will be terminated, the thread exits. For the main thread, we do not want to be run for a period of time, and then exit, so how to ensure that it is always alive? Binder threads, for example, also use an infinite loop to write and write to binder drivers in a different way. Of course, they do not simply loop forever, falling asleep when there is no message. But that might raise another question: how do you do anything else if it’s an infinite loop? By creating a new thread. Really getting stuck the operation of the main thread is in the callback method onCreate/onStart/onResume operating time is too long, can lead to a frame, even ANR, stars. The loop itself does not result in application.

(2) Does the main thread run in an endless loop consume CPU resources? If the main thread MessageQueue has no message, it blocks the nativePollOnce() method in the loop queue.next(). The main thread then releases CPU resources and goes to sleep until the next message arrives or a transaction occurs, waking up the main thread by writing data to the pipe end. The epoll mechanism adopted here is an IO multiplexing mechanism, which can monitor multiple descriptors at the same time. When a descriptor is ready (read or write ready), it immediately notifies the corresponding program to carry out read or write operations, which is essentially synchronous I/O, that is, read and write is blocked. Therefore, the main thread is dormant most of the time and does not consume a lot of CPU resources

What is the message loop mechanism for the main thread

New binder threads are created before the main thread goes into an infinite loop, in code activityThread.main () :

public static void main(String[] args) {

// Create Looper and MessageQueue objects to handle messages for the main thread

Looper.prepareMainLooper();

// Create an ActivityThread object

ActivityThread thread = new ActivityThread();

// Create a new thread.

thread.attach(false);

Looper.loop(); // The message loop runs

throw new RuntimeException("Main thread loop unexpectedly exited");

}
Copy the code
  • The Activity’s life cycle depends on the main thread’s looper. loop, which takes action when different messages are received: Once you exit the Message loop, your program can exit. Fetching a message from a message queue may block and will be processed accordingly. If a message takes too long to process, it can affect the refresh rate of the UI thread and cause a lag.
  • The Attach (false) method creates a Binder thread (ApplicationThread, the server side of the Binder that receives events from AMS). The Binder thread sends messages to the main thread through a Handler. “The Activity start process such as receive MSG = H.L AUNCH_ACTIVITY, call the ActivityThread. HandleLaunchActivity () method, which will eventually through reflection mechanism, create the Activity instance, Then execute methods such as activity.oncreate ();
  • Such as MSG = have received H.P AUSE_ACTIVITY, call the ActivityThread. HandlePauseActivity () method, which will eventually perform the Activity. The onPause () method.
  • Where does the main thread come from? Of course, other threads in the App process send messages to the main thread via Handler

The Handler mechanism is explained in detail

The Handler mechanism mainly involves the following four classes, which have clear division of labor but interact with each other

Message: the Message

Hanlder: Originator of the message

Looper: News traverser

MessageQueue: MessageQueue

1, stars. Prepare ()

public static void prepare(a) {

prepare(true);

}

private static void prepare(boolean quitAllowed) {

// A thread can only call Looper once. Prepare ()

if(sThreadLocal.get() ! =null) {

throw new RuntimeException("Only one Looper may be created per thread");

}

// If the current thread does not have a Looper, create one and store it in sThreadLocal

sThreadLocal.set(new Looper(quitAllowed));

}
Copy the code

As you can see from the code above, a thread has at most one Looper object. SThreadLocal is a static ThreadLocal object. SThreadLocal is a static ThreadLocal object, but it stores a copy of the Looper object. It also lets you get a copy of the Looper previously stored by the current thread. The diagram below:

Now look at the Looper constructor:

private Looper(boolean quitAllowed) {

// Create MessageQueue and hold it for Looper

mQueue = new MessageQueue(quitAllowed);

// Let Looper hold the current thread object

mThread = Thread.currentThread();

}
Copy the code

The MessageQueue MessageQueue is created and held by Looper. Since a thread can only have one Looper object at most, a thread can only have one MessageQueue at most. The current thread is then assigned to the mThread.

The constructor of MessageQueue is nothing more than a Message queue to hold messages.

Therefore, looper.prepare () plays the following roles:

  • Create a Looper object
  • Create a MessageQueue object and let the Looper object hold it
  • Let the Looper object hold the current thread

2, the new Handler ()

The Handler constructor provides a custom Callback, Looper, etc. We’ll start with the simplest constructor with no arguments:

public Handler(a) {

this(null.false);

}

public Handler(Callback callback, boolean async) {

// Irrelevant code.// Get the current thread's Looper, which is called sthreadLocal.get

mLooper = Looper.myLooper();

// Raise a runtime exception if the current thread has no Looper

if (mLooper == null) {

throw new RuntimeException(

"Can't create handler inside thread that has not called Looper.prepare()");

}

// Let the Handler hold the resulting MessagQueue

mQueue = mLooper.mQueue;

// Initialize the Handler Callback, which is actually the 2 of the original Callback method in the diagram

mCallback = callback;

mAsynchronous = async;

}
Copy the code

First, we call looper. myLooper (sthreadLocal. get) and get the Looper object saved by the current thread’s sThreadLocal.set call and let the Handler hold it. It then determines whether the resulting Looper object is empty, and if so, it is reported

“Can’t create handler inside thread that has not called Looper. Prepare (), This is the error we reported earlier when we created Handler in the child thread without calling Looper.prepare. Indeed, when we do not call looper.prepare (), there is no Looper object in the current thread.

Then, let the Handler hold the MessageQueue of the resulting Looper object and set the Callback object to handle the Callback (Callback method 2 in the original figure).

At this point, the creation of the default Handler is complete, mainly with the following points:

  • Creating a Handler object
  • Gets the Looper object of the current thread and determines whether it is empty
  • Create Handler objects that hold references to Looper, MessageQueu, and Callback

3, stars. Loop ()

Public static void loop() {// Get the current thread's Looper object 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; // Irrelevant code...... // loop for (;;) MSG = queue.next(); {// Retrieve Message from MessageQueue, block Message MSG = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } / / the Message. The target is Handler, is sending a Message Handler, here is the dispatchMessage method call it MSG. Target. DispatchMessage (MSG); // Discard Message msg.recycleunchecked (); }}Copy the code

First, it determines whether the current thread has a Looper, and then obtains the MessageQueue of the current thread. Next, the most critical code, is to write an endless loop, calling MessageQueue’s next method to retrieve the Message in the MessageQueue. Note that when there is no Message in the MessageQueue, the next method blocks, causing the current thread to hang, as we’ll see later.

Once we get the Message, we call its dispatchMessage method for target, which is essentially the Handler used to send the Message. So we call Handler’s dispatchMessage method as follows:

Public void dispatchMessage(Message MSG) {// If msg.callback is not null, handleCallback if (msg.callback! = null) { handleCallback(msg); } else {// If mCallback is not empty, call McAllback. handleMessage if (mCallback! = null) { if (mCallback.handleMessage(msg)) { return; }} // Call the Handler's own handleMessage, which is the method we often override handleMessage(MSG); }}Copy the code

As you can see, this method is to fetch the Message from the MessageQueue and distribute it.

MSG. Callback is a Runnable object passed in by handler. post, as we’ll see later. And hanldeCallback is the Runnable run method called.

This is an interface type for handler. Callback. Handler has multiple constructors that can set the Callback. If this is not null, call its hanldeMessage method. If true is returned, Handler’s handleMessage method is no longer called. If the mCallback is empty, or not empty but its handleMessage returns false, then the Handler’s handleMessage method will continue to be called, which is the one we often override.

The following flow chart shows the distribution of messages after they are taken out of MessageQueue:

So looper. loop does this:

Message is continually fetched from the current thread’s MessageQueue and its associated callback method is called.

4. Send messages

There are two main ways to send messages using Handler, sendXXXMessage and postXXX, but both methods end up calling sendMessageDelayed, so we’ll use the simplest sendMessage method.

Let’s start with Handler’s sendMessage method:

public final boolean sendMessage(Message msg) { return sendMessageDelayed(msg, 0); } public final boolean sendMessageDelayed(Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); } public Boolean sendMessageAtTime(Message MSG, long uptimeMillis) {public Boolean sendMessageAtTime(Message MSG, long uptimeMillis) MessageQueue queue = mQueue; MessageQueue queue = mQueue; if (queue == null) { RuntimeException e = new RuntimeException( this + " sendMessageAtTime() called with no mQueue"); Log.w("Looper", e.getMessage(), e); return false; } // Call enqueueMessage(queue, MSG, uptimeMillis) to add the message to MessageQueue. } the main implementation is to call enqueueMessage: Private Boolean enqueueMessage(MessageQueue Queue, Message MSG, long uptimeMillis) { MSG. Target = this; MSG. if (mAsynchronous) { msg.setAsynchronous(true); } return queuemessage (MSG, uptimeMillis); }Copy the code

First, we use the current Handler as the target property of the Message to facilitate Message processing when Looper retrieves the Message from the MessageQueue. Then the enqueueMessage method of MessageQueue is called to add the message sent by the handler to MessageQueue for Looper to retrieve and process. Let’s take a look at MessageQueue’s enqueueMessage method:

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("MessageQueue", e.getMessage(), e); msg.recycle(); return false; } // the Message tag already uses msg.markinuse (); msg.when = when; Message p = mMessages; boolean needWake; / / when we here is zero, if the news of the said immediately (p = = null | | the when = = 0 | | the when < p.w hen) {/ / the message is inserted into the message queue head MSG. The next = p; mMessages = msg; needWake = mBlocked; 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; }} // insert the message into the appropriate position msg.next = p; // invariant: p == prev.next prev.next = msg; } // If the queue is blocked, wake up if (needWake) {nativeWake(mPtr); } } return true; }Copy the code

If yes, an exception will be thrown. This is understandable. If a Message already exists in MessageQueue, but has not been processed, if the Message is sent again, This can cause problems when processing the previous Message.

And then we’re going to judge when, which is the time of delay, and we have no delay here, so it’s 0, which satisfies the if condition. Inserts the message to the head of the message queue. If when is not 0, the message needs to be added to the appropriate location in the message queue.

Finally, it determines whether the current thread is blocked, and if it is, it needs to call a local method to wake it up.

That’s all the sendMessage process is all about adding the Message to the appropriate location in the MessageQueue. Let’s take a quick look at the POST series:

public final boolean post(Runnable r) { return sendMessageDelayed(getPostMessage(r), 0); } private static Message getPostMessage(Runnable r) { And get its callback to execute the Runnable Message m = message.obtain (); m.callback = r; return m; }Copy the code

As you can see, the POST method simply calls getPostMessage to encapsulate a Message with Runnable, and then calls sendMessageDelayed to add the encapsulated Message to the MessageQueue.

So the essence of sending messages using a handler is to add a Message to the MessageQueue in the handler.

How does Handler handle thread switching

The Handler is created using the current thread’s Looper to construct the message loop. The Looper is bound to the thread in which it was created, and the Handler processes the message in the thread in which it is associated with the Looper.

So how does the Handler internally get the Looper of the current thread — ThreadLocal?

ThreadLocal can store and provide data in different threads without interfering with each other. ThreadLocal can easily fetch Looper for each thread. Note, of course:

Threads do not have Looper by default. If you want to use Handler, you must create Looper for the thread. The main thread that we talk about a lot, also called the UI thread, is an ActivityThread;

Looper is initialized when an ActivityThread is created. This is why we can use Handler in the main thread by default.

Why does the system not allow access to the UI in child threads? This is because Android UI controls are not thread-safe. If concurrent access in multiple threads can cause the UI controls to be in an unexpected state, then why does the system not lock access to the UI controls? There are two disadvantages: (1) adding locking mechanism will complicate the UI access logic and (2) locking mechanism will reduce the EFFICIENCY of UI access, because locking mechanism will block the execution of some threads. So the simplest and most efficient way to handle UI operations is to adopt a single-threaded model.

4. Handler causes memory leaks

1. Causes of memory leaks

Java uses a directed graph mechanism that automatically checks for objects in memory through GC (at which time is up to the virtual machine), and if the GC finds one or a group of objects to be unreachable, the object is reclaimed from memory. That is, if an object is not referred to by any reference, it will be reclaimed when it is discovered by the GC. In addition, if A set of objects contains only references to each other, but no references from outside them (for example, two objects A and B hold references to each other, but no external objects hold references to A or B), this is still unreachable and will also be collected by GC.

The use of Handlers in Android causes memory leaks

Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { mImageView.setImageBitmap(mBitmap); }}Copy the code

Above is a simple example of how to use Handler. When creating a Handler using an internal class (including an anonymous class), the Handler object implicitly holds a reference to an external class object (usually an Activity). . The Handler is usually accompanied by a time-consuming background thread (such as pulling an image from the network) that notifies the Handler via a message mechanism when the task is complete (such as downloading an image) and the Handler updates the image to the interface. However, if the user closes the Activity during a network request, the Activity is normally no longer in use, and it might be reclaimed during a GC check, but since the thread has not yet finished executing and holds a reference to Handler (how else would it send a message to Handler?). This Handler in turn holds a reference to the Activity, preventing the Activity from being recycled (i.e., a memory leak) until the network request ends (e.g., an image is downloaded). Also, if you execute Handler’s postDelayed() method, which loads your Handler into a Message and pushes the Message to the MessageQueue, then until the delay you set arrives, There will be a chain of MessageQueue -> Message -> Handler -> Activity, causing your Activity to hold a reference and not be recycled.

2. Solutions

① Stop your background thread while closing the Activity. When the thread stops, the Handler is disconnected from the external connection, and the Activity is automatically reclaimed at the appropriate time.

If your Handler is referenced by the delay Message, remove the Message object from the queue using the corresponding Handler removeCallbacks() method.

Declare the Handler as a static class. Since the Handler no longer holds references to external class objects, the program will no longer allow you to operate on objects in your Activity in the Handler. So you need to add a WeakReference to the Activity in the Handler.

private static class MyHandler extends Handler { WeakReference<MainActivity> mActivity; MyHandler(MainActivity mActivity){ this.mActivity = new WeakReference<MainActivity>(mActivity); } @Override public void handleMessage(Message msg) { switch(msg.what){ case IMAGE_FAILURE: Toast.makeText(mActivity.get() , "Image Failure", Toast.LENGTH_LONG).show(); break; }}}Copy the code

conclusion

The Handler messaging mechanism provides four main classes of functionality:

Message: The carrier of a Message, which holds a Handler, stored in a MessageQueue, which can have more than one thread

Hanlder: The originator of a Message, the callback implementation that sends the Message and handles the Message. A thread can have multiple Handler objects

Looper: Message traverser, looping out messages from MessageQueue for processing, at most one per thread

MessageQueue: MessageQueue containing messages sent by Handler for Looper loop to retrieve messages, at most one per thread